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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +170 -0
  3. data/Rakefile +33 -0
  4. data/bin/piggly +8 -0
  5. data/lib/piggly/command/base.rb +148 -0
  6. data/lib/piggly/command/report.rb +162 -0
  7. data/lib/piggly/command/trace.rb +90 -0
  8. data/lib/piggly/command/untrace.rb +78 -0
  9. data/lib/piggly/command.rb +8 -0
  10. data/lib/piggly/compiler/cache_dir.rb +119 -0
  11. data/lib/piggly/compiler/coverage_report.rb +63 -0
  12. data/lib/piggly/compiler/trace_compiler.rb +117 -0
  13. data/lib/piggly/compiler.rb +7 -0
  14. data/lib/piggly/config.rb +80 -0
  15. data/lib/piggly/dumper/index.rb +121 -0
  16. data/lib/piggly/dumper/qualified_name.rb +36 -0
  17. data/lib/piggly/dumper/qualified_type.rb +141 -0
  18. data/lib/piggly/dumper/reified_procedure.rb +172 -0
  19. data/lib/piggly/dumper/skeleton_procedure.rb +112 -0
  20. data/lib/piggly/dumper.rb +9 -0
  21. data/lib/piggly/installer.rb +137 -0
  22. data/lib/piggly/parser/grammar.tt +748 -0
  23. data/lib/piggly/parser/nodes.rb +378 -0
  24. data/lib/piggly/parser/traversal.rb +50 -0
  25. data/lib/piggly/parser/treetop_ruby19_patch.rb +21 -0
  26. data/lib/piggly/parser.rb +69 -0
  27. data/lib/piggly/profile.rb +108 -0
  28. data/lib/piggly/reporter/base.rb +106 -0
  29. data/lib/piggly/reporter/html_dsl.rb +63 -0
  30. data/lib/piggly/reporter/index.rb +114 -0
  31. data/lib/piggly/reporter/procedure.rb +129 -0
  32. data/lib/piggly/reporter/resources/highlight.js +38 -0
  33. data/lib/piggly/reporter/resources/piggly.css +515 -0
  34. data/lib/piggly/reporter/resources/sortable.js +493 -0
  35. data/lib/piggly/reporter.rb +8 -0
  36. data/lib/piggly/tags.rb +280 -0
  37. data/lib/piggly/task.rb +215 -0
  38. data/lib/piggly/util/blankslate.rb +114 -0
  39. data/lib/piggly/util/cacheable.rb +19 -0
  40. data/lib/piggly/util/enumerable.rb +44 -0
  41. data/lib/piggly/util/file.rb +17 -0
  42. data/lib/piggly/util/process_queue.rb +96 -0
  43. data/lib/piggly/util/thunk.rb +39 -0
  44. data/lib/piggly/util.rb +9 -0
  45. data/lib/piggly/version.rb +15 -0
  46. data/lib/piggly.rb +20 -0
  47. data/spec/examples/compiler/cacheable_spec.rb +190 -0
  48. data/spec/examples/compiler/report_spec.rb +25 -0
  49. data/spec/examples/compiler/trace_spec.rb +123 -0
  50. data/spec/examples/config_spec.rb +63 -0
  51. data/spec/examples/dumper/index_spec.rb +199 -0
  52. data/spec/examples/dumper/procedure_spec.rb +116 -0
  53. data/spec/examples/grammar/expression_spec.rb +302 -0
  54. data/spec/examples/grammar/statements/assignment_spec.rb +70 -0
  55. data/spec/examples/grammar/statements/declaration_spec.rb +21 -0
  56. data/spec/examples/grammar/statements/exception_spec.rb +78 -0
  57. data/spec/examples/grammar/statements/if_spec.rb +191 -0
  58. data/spec/examples/grammar/statements/loop_spec.rb +41 -0
  59. data/spec/examples/grammar/statements/sql_spec.rb +71 -0
  60. data/spec/examples/grammar/tokens/comment_spec.rb +58 -0
  61. data/spec/examples/grammar/tokens/datatype_spec.rb +58 -0
  62. data/spec/examples/grammar/tokens/identifier_spec.rb +74 -0
  63. data/spec/examples/grammar/tokens/keyword_spec.rb +44 -0
  64. data/spec/examples/grammar/tokens/label_spec.rb +40 -0
  65. data/spec/examples/grammar/tokens/literal_spec.rb +30 -0
  66. data/spec/examples/grammar/tokens/lval_spec.rb +50 -0
  67. data/spec/examples/grammar/tokens/number_spec.rb +34 -0
  68. data/spec/examples/grammar/tokens/sqlkeywords_spec.rb +45 -0
  69. data/spec/examples/grammar/tokens/string_spec.rb +54 -0
  70. data/spec/examples/grammar/tokens/whitespace_spec.rb +40 -0
  71. data/spec/examples/installer_spec.rb +59 -0
  72. data/spec/examples/parser/nodes_spec.rb +73 -0
  73. data/spec/examples/parser/traversal_spec.rb +14 -0
  74. data/spec/examples/parser_spec.rb +118 -0
  75. data/spec/examples/profile_spec.rb +153 -0
  76. data/spec/examples/reporter/html/dsl_spec.rb +0 -0
  77. data/spec/examples/reporter/html/index_spec.rb +0 -0
  78. data/spec/examples/reporter/html_spec.rb +1 -0
  79. data/spec/examples/reporter_spec.rb +0 -0
  80. data/spec/examples/tags_spec.rb +285 -0
  81. data/spec/examples/task_spec.rb +0 -0
  82. data/spec/examples/util/cacheable_spec.rb +41 -0
  83. data/spec/examples/util/enumerable_spec.rb +64 -0
  84. data/spec/examples/util/file_spec.rb +40 -0
  85. data/spec/examples/util/process_queue_spec.rb +16 -0
  86. data/spec/examples/util/thunk_spec.rb +59 -0
  87. data/spec/examples/version_spec.rb +0 -0
  88. data/spec/issues/007_spec.rb +25 -0
  89. data/spec/issues/008_spec.rb +73 -0
  90. data/spec/issues/018_spec.rb +25 -0
  91. data/spec/issues/028_spec.rb +48 -0
  92. data/spec/issues/032_spec.rb +98 -0
  93. data/spec/issues/036_spec.rb +41 -0
  94. data/spec/spec_helper.rb +312 -0
  95. data/spec/spec_suite.rb +5 -0
  96. metadata +162 -0
@@ -0,0 +1,280 @@
1
+ module Piggly
2
+ #
3
+ # Coverage is tracked by attaching these compiler-generated tags to various nodes in a stored
4
+ # procedure's parse tree. These tags each have a unique string identifier which is printed by
5
+ # various parts of the recompiled stored procedure, and the output is then recognized by
6
+ # Profile.notice_processor, which calls #ping on the tag corresponding to the printed string.
7
+ #
8
+ # After test execution is complete, each AST is walked and Tag values attached to NodeClass
9
+ # values are used to produce the coverage report
10
+ #
11
+ module Tags
12
+
13
+ class AbstractTag
14
+ PATTERN = /[0-9a-f]{16}/
15
+
16
+ attr_accessor :id
17
+
18
+ def initialize(prefix = nil, id = nil)
19
+ @id = Digest::MD5.hexdigest(prefix.to_s + (id || object_id).to_s).slice(0, 16)
20
+ end
21
+
22
+ alias to_s id
23
+
24
+ # Defined here in case ActiveSupport hasn't defined it on Object
25
+ def tap
26
+ yield self
27
+ self
28
+ end
29
+ end
30
+
31
+ class EvaluationTag < AbstractTag
32
+ attr_reader :ran
33
+
34
+ def initialize(*args)
35
+ clear
36
+ super
37
+ end
38
+
39
+ def type
40
+ :block
41
+ end
42
+
43
+ def ping(value)
44
+ @ran = true
45
+ end
46
+
47
+ def style
48
+ "c#{@ran ? "1" : "0"}"
49
+ end
50
+
51
+ def to_f
52
+ @ran ? 100.0 : 0.0
53
+ end
54
+
55
+ def complete?
56
+ @ran
57
+ end
58
+
59
+ def description
60
+ @ran ? "full coverage" : "never evaluated"
61
+ end
62
+
63
+ # Resets code coverage
64
+ def clear
65
+ @ran = false
66
+ end
67
+
68
+ def ==(other)
69
+ @id == other.id and @ran == other.ran
70
+ end
71
+ end
72
+
73
+ #
74
+ # Tracks a contiguous sequence of statements
75
+ #
76
+ class BlockTag < EvaluationTag
77
+ end
78
+
79
+ #
80
+ # Tracks procedure calls, raise exception, exits, returns
81
+ #
82
+ class UnconditionalBranchTag < EvaluationTag
83
+ # Aggregate this coverage data with conditional branches
84
+ def type
85
+ :branch
86
+ end
87
+ end
88
+
89
+ #
90
+ # Tracks if, catch, case branch, continue when, and exit when statements
91
+ # where the coverage consists of the condition evaluating true and false
92
+ #
93
+ class ConditionalBranchTag < AbstractTag
94
+ attr_reader :true, :false
95
+
96
+ def initialize(*args)
97
+ clear
98
+ super
99
+ end
100
+
101
+ def type
102
+ :branch
103
+ end
104
+
105
+ def ping(value)
106
+ case value
107
+ when "t"; @true = true
108
+ when "f"; @false = true
109
+ end
110
+ end
111
+
112
+ def style
113
+ "b#{@true ? 1 : 0}#{@false ? 1 : 0 }"
114
+ end
115
+
116
+ def to_f
117
+ (@true and @false) ? 100.0 : (@true or @false) ? 50.0 : 0.0
118
+ end
119
+
120
+ def complete?
121
+ @true and @false
122
+ end
123
+
124
+ def description
125
+ if @true and @false
126
+ "full coverage"
127
+ elsif @true
128
+ "never evaluates false"
129
+ elsif @false
130
+ "never evaluates true"
131
+ else
132
+ "never evaluated"
133
+ end
134
+ end
135
+
136
+ def clear
137
+ @true, @false = false
138
+ end
139
+
140
+ def ==(other)
141
+ @id == other.id and @true == other.true and @false == other.false
142
+ end
143
+ end
144
+
145
+ #
146
+ # Tracks loops where coverage consists of iterating once, iterating more
147
+ # than once, passing through, and at least one full iteration
148
+ #
149
+ class AbstractLoopTag < AbstractTag
150
+ def self.states
151
+ { # Never terminates normally (so @pass must be false)
152
+ 0b0000 => "never evaluated",
153
+ 0b0001 => "iterations always terminate early. loop always iterates more than once",
154
+ 0b0010 => "iterations always terminate early. loop always iterates only once",
155
+ 0b0011 => "iterations always terminate early",
156
+ # Terminates normally (one of @pass, @once, @twice must be true)
157
+ 0b1001 => "loop always iterates more than once",
158
+ 0b1010 => "loop always iterates only once",
159
+ 0b1011 => "loop never passes through",
160
+ 0b1100 => "loop always passes through",
161
+ 0b1101 => "loop never iterates only once",
162
+ 0b1110 => "loop never iterates more than once",
163
+ 0b1111 => "full coverage" }
164
+ end
165
+
166
+ attr_reader :pass, :once, :twice, :ends, :count
167
+
168
+ def initialize(*args)
169
+ clear
170
+ super
171
+ end
172
+
173
+ def type
174
+ :loop
175
+ end
176
+
177
+ def style
178
+ "l#{[@pass, @once, @twice, @ends].map{|b| b ? 1 : 0}}"
179
+ end
180
+
181
+ def to_f
182
+ # Value space:
183
+ # (1,2,X) - loop iterated at least twice and terminated normally
184
+ # (1,X) - loop iterated only once and terminated normally
185
+ # (0,X) - loop never iterated and terminated normally (pass-thru)
186
+ # () - loop condition was never executed
187
+ #
188
+ # These combinations are ignored, because coverage will probably not reveal bugs
189
+ # (1,2) - loop iterated at least twice but terminated early
190
+ # (1) - loop iterated only once but terminated early
191
+ 100 * (Util::Enumerable.count([@pass, @once, @twice, @ends]){|x| x } / 4.0)
192
+ end
193
+
194
+ def complete?
195
+ @pass and @once and @twice and @ends
196
+ end
197
+
198
+ def description
199
+ self.class.states.fetch(n = state, "unknown tag state: #{n}")
200
+ end
201
+
202
+ # Returns state represented as a 4-bit integer
203
+ def state
204
+ [@ends,@pass,@once,@twice].reverse.inject([0,0]){|(k,n), bit| [k + 1, n | (bit ? 1 : 0) << k] }.last
205
+ end
206
+
207
+ def clear
208
+ @pass = false
209
+ @once = false
210
+ @twice = false
211
+ @ends = false
212
+ @count = 0
213
+ end
214
+
215
+ def ==(other)
216
+ @id == other.id and
217
+ @ends == other.ends and
218
+ @pass == other.pass and
219
+ @once == other.once and
220
+ @twice == other.twice
221
+ end
222
+ end
223
+
224
+ #
225
+ # Tracks loops that have a boolean condition in the loop statement (WHILE loops)
226
+ #
227
+ class ConditionalLoopTag < AbstractLoopTag
228
+ def ping(value)
229
+ case value
230
+ when "t"
231
+ # Loop iterated
232
+ @count += 1
233
+ else
234
+ # Loop terminated
235
+ case @count
236
+ when 0; @pass = true
237
+ when 1; @once = true
238
+ else; @twice = true
239
+ end
240
+ @count = 0
241
+
242
+ # This isn't accurate. there needs to be a signal at the end
243
+ # of the loop body to indicate it was reached. Otherwise its
244
+ # possible each iteration restarts early with CONTINUE
245
+ @ends = true
246
+ end
247
+ end
248
+ end
249
+
250
+ #
251
+ # Tracks loops that don't have a boolean condition in the loop statement (LOOP and FOR loops)
252
+ #
253
+ class UnconditionalLoopTag < AbstractLoopTag
254
+ def self.states
255
+ super.merge \
256
+ 0b0100 => "loop always passes through"
257
+ end
258
+
259
+ def ping(value)
260
+ case value
261
+ when "t"
262
+ # start of iteration
263
+ @count += 1
264
+ when "@"
265
+ # end of iteration
266
+ @ends = true
267
+ when "f"
268
+ # loop exit
269
+ case @count
270
+ when 0; @pass = true
271
+ when 1; @once = true
272
+ else; @twice = true
273
+ end
274
+ @count = 0
275
+ end
276
+ end
277
+ end
278
+
279
+ end
280
+ end
@@ -0,0 +1,215 @@
1
+ require "rake"
2
+ require "rake/tasklib"
3
+
4
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), ".."))
5
+ require "piggly"
6
+
7
+ module Piggly
8
+
9
+ class AbstractTask < Rake::TaskLib
10
+ attr_accessor :name, # Name of the test task
11
+ :verbose,
12
+ :ruby_opts
13
+
14
+ attr_accessor :procedures, # List of procedure names or regular expressions, match all by default
15
+ :cache_root, # Where to store cache data
16
+ :piggly_opts,
17
+ :piggly_path # Path to bin/piggly
18
+
19
+ def initialize(name = :piggly)
20
+ @name = name
21
+ @verbose = false
22
+ @ruby_opts = []
23
+
24
+ @procedures = []
25
+ @cache_root = nil
26
+ @piggly_path = File.expand_path("#{File.dirname(__FILE__)}/../../bin/piggly")
27
+ @piggly_opts = []
28
+
29
+ yield self if block_given?
30
+ define
31
+ end
32
+
33
+ private
34
+
35
+ def quote(value)
36
+ case value
37
+ when Regexp
38
+ quote(value.inspect)
39
+ else
40
+ %{"#{value}"}
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ class TraceTask < AbstractTask
47
+ def initialize(name = :trace)
48
+ super(name)
49
+ end
50
+
51
+ private
52
+
53
+ def define
54
+ desc 'Trace stored procedures'
55
+ task @name do
56
+ RakeFileUtils.verbose(@verbose) do
57
+ opts = []
58
+ opts << "trace"
59
+ opts.concat(["--cache-root", @cache_root]) if @cache_root
60
+
61
+ case @procedures
62
+ when String then opts.concat(["--name", @procedures])
63
+ when Regexp then opts.concat(["--name", @procedures.inspect])
64
+ when Array
65
+ @procedures.each do |p|
66
+ case p
67
+ when String then opts.concat(["--name", p])
68
+ when Regexp then opts.concat(["--name", p.inspect])
69
+ end
70
+ end
71
+ end
72
+
73
+ opts.concat(@piggly_opts)
74
+ # ruby(opts.join(" "))
75
+ Command.main(opts)
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ class UntraceTask < AbstractTask
82
+ def initialize(name = :untrace)
83
+ super(name)
84
+ end
85
+
86
+ private
87
+
88
+ def define
89
+ desc 'Untrace stored procedures'
90
+ task @name do
91
+ RakeFileUtils.verbose(@verbose) do
92
+ # opts = @ruby_opts.clone
93
+ # opts << (@piggly_path ? quote(@piggly_path) : "-S piggly")
94
+ opts = []
95
+ opts << "untrace"
96
+ opts.concat(["--cache-root", @cache_root]) if @cache_root
97
+
98
+ case @procedures
99
+ when String then opts.concat(["--name", @procedures])
100
+ when Regexp then opts.concat(["--name", @procedures.inspect])
101
+ when Array
102
+ @procedures.each do |p|
103
+ case p
104
+ when String then opts.concat(["--name", p])
105
+ when Regexp then opts.concat(["--name", p.inspect])
106
+ end
107
+ end
108
+ end
109
+
110
+ opts.concat(@piggly_opts)
111
+ # ruby(opts.join(" "))
112
+ Command.main(opts)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ class ReportTask < AbstractTask
119
+ attr_accessor :report_root, # Where to store reports (default piggly/report)
120
+ :accumulate, # Accumulate coverage from the previous run (default false)
121
+ :trace_file
122
+
123
+ def initialize(name = :report)
124
+ @accumulate = false
125
+ @trace_file = nil
126
+ @report_root = nil
127
+ super(name)
128
+ end
129
+
130
+ private
131
+
132
+ def define
133
+ desc 'Generate piggly report'
134
+ task @name do
135
+ RakeFileUtils.verbose(@verbose) do
136
+ # opts = @ruby_opts.clone
137
+ # opts << (@piggly_path ? quote(@piggly_path) : "-S piggly")
138
+ opts = []
139
+ opts << "report"
140
+ opts << "--accumulate" if @accumulate
141
+ opts.concat(["--trace-file", @trace_file])
142
+ opts.concat(["--cache-root", @cache_root]) if @cache_root
143
+ opts.concat(["--report-root", @report_root]) if @report_root
144
+
145
+ case @procedures
146
+ when String then opts.concat(["--name", @procedures])
147
+ when Regexp then opts.concat(["--name", @procedures.inspect])
148
+ when Array
149
+ @procedures.each do |p|
150
+ case p
151
+ when String then opts.concat(["--name", p])
152
+ when Regexp then opts.concat(["--name", p.inspect])
153
+ end
154
+ end
155
+ end
156
+
157
+ opts.concat(@piggly_opts)
158
+ # ruby(opts.join(" "))
159
+ Command.main(opts)
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ class TestTask < AbstractTask
166
+ attr_accessor :test_files, # List of ruby test files to load
167
+ :report_root, # Where to store reports (default piggly/report)
168
+ :accumulate # Accumulate coverage from the previous run (default false)
169
+
170
+ def initialize(name = :piggly)
171
+ @report_root = nil
172
+ @test_files = []
173
+ @accumulate = false
174
+ super(name)
175
+ end
176
+
177
+ private
178
+
179
+ def define
180
+ desc 'Run piggly tests' + (@name == :piggly ? '' : " for #{@name}")
181
+ task @name do
182
+ RakeFileUtils.verbose(@verbose) do
183
+ opts = @ruby_opts.clone
184
+ opts << (@piggly_path ? quote(@piggly_path) : "-S piggly")
185
+ opts << "test"
186
+ opts << "--accumulate" if @accumulate
187
+ opts << "--cache-root #{quote @cache_root}" if @cache_root
188
+ opts << "--report-root #{quote @report_root}" if @report_root
189
+
190
+ case @procedures
191
+ when String then opts << "--name #{quote @procedures}"
192
+ when Regexp then opts << "--name #{quote @procedures.inspect}"
193
+ when Array
194
+ @procedures.each do |p|
195
+ case p
196
+ when String then opts << "--name #{quote p}"
197
+ when Regexp then opts << "--name #{quote p.inspect}"
198
+ end
199
+ end
200
+ end
201
+
202
+ opts.concat(@piggly_opts)
203
+
204
+ unless (@test_files || []).empty?
205
+ opts << "--"
206
+ opts.concat(@test_files.map{|x| quote(x) })
207
+ end
208
+
209
+ ruby(opts.join(" "))
210
+ end
211
+ end
212
+ end
213
+ end
214
+
215
+ end
@@ -0,0 +1,114 @@
1
+ unless defined? BlankSlate
2
+ #--
3
+ # Copyright 2004, 2006 by Jim Weirich (jim@weirichhouse.org).
4
+ # All rights reserved.
5
+
6
+ # Permission is granted for use, copying, modification, distribution,
7
+ # and distribution of modified versions of this work as long as the
8
+ # above copyright notice is included.
9
+ #++
10
+
11
+ ######################################################################
12
+ # BlankSlate provides an abstract base class with no predefined
13
+ # methods (except for <tt>\_\_send__</tt> and <tt>\_\_id__</tt>).
14
+ # BlankSlate is useful as a base class when writing classes that
15
+ # depend upon <tt>method_missing</tt> (e.g. dynamic proxies).
16
+ #
17
+ class BlankSlate
18
+ class << self
19
+
20
+ # Hide the method named +name+ in the BlankSlate class. Don't
21
+ # hide +instance_eval+ or any method beginning with "__".
22
+ def hide(name)
23
+ if instance_methods.include?(name.to_s) and
24
+ name !~ /^(__|instance_eval)/
25
+ @hidden_methods ||= {}
26
+ @hidden_methods[name.to_sym] = instance_method(name)
27
+ undef_method name
28
+ end
29
+ end
30
+
31
+ def find_hidden_method(name)
32
+ @hidden_methods ||= {}
33
+ @hidden_methods[name] || superclass.find_hidden_method(name)
34
+ end
35
+
36
+ # Redefine a previously hidden method so that it may be called on a blank
37
+ # slate object.
38
+ def reveal(name)
39
+ bound_method = nil
40
+ unbound_method = find_hidden_method(name)
41
+ fail "Don't know how to reveal method '#{name}'" unless unbound_method
42
+ define_method(name) do |*args|
43
+ bound_method ||= unbound_method.bind(self)
44
+ bound_method.call(*args)
45
+ end
46
+ end
47
+ end
48
+
49
+ instance_methods.each { |m| hide(m) }
50
+ end
51
+
52
+ ######################################################################
53
+ # Since Ruby is very dynamic, methods added to the ancestors of
54
+ # BlankSlate <em>after BlankSlate is defined</em> will show up in the
55
+ # list of available BlankSlate methods. We handle this by defining a
56
+ # hook in the Object and Kernel classes that will hide any method
57
+ # defined after BlankSlate has been loaded.
58
+ #
59
+ module Kernel
60
+ class << self
61
+ alias_method :blank_slate_method_added, :method_added
62
+
63
+ # Detect method additions to Kernel and remove them in the
64
+ # BlankSlate class.
65
+ def method_added(name)
66
+ result = blank_slate_method_added(name)
67
+ return result if self != Kernel
68
+ BlankSlate.hide(name)
69
+ result
70
+ end
71
+ end
72
+ end
73
+
74
+ ######################################################################
75
+ # Same as above, except in Object.
76
+ #
77
+ class Object
78
+ class << self
79
+ alias_method :blank_slate_method_added, :method_added
80
+
81
+ # Detect method additions to Object and remove them in the
82
+ # BlankSlate class.
83
+ def method_added(name)
84
+ result = blank_slate_method_added(name)
85
+ return result if self != Object
86
+ BlankSlate.hide(name)
87
+ result
88
+ end
89
+
90
+ def find_hidden_method(name)
91
+ nil
92
+ end
93
+ end
94
+ end
95
+
96
+ ######################################################################
97
+ # Also, modules included into Object need to be scanned and have their
98
+ # instance methods removed from blank slate. In theory, modules
99
+ # included into Kernel would have to be removed as well, but a
100
+ # "feature" of Ruby prevents late includes into modules from being
101
+ # exposed in the first place.
102
+ #
103
+ class Module
104
+ alias blankslate_original_append_features append_features
105
+ def append_features(mod)
106
+ result = blankslate_original_append_features(mod)
107
+ return result if mod != Object
108
+ instance_methods.each do |name|
109
+ BlankSlate.hide(name)
110
+ end
111
+ result
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,19 @@
1
+ module Piggly
2
+ module Util
3
+ module Cacheable
4
+
5
+ def cache_path(file)
6
+ # Up to the last capitalized word of the class name
7
+ classdir = self.class.name[/^(?:.+::)?(.+?)([A-Z][^A-Z]+)?$/, 1]
8
+
9
+ # md5 the full path to prevent collisions
10
+ full = ::File.expand_path(file)
11
+ hash = Digest::MD5.hexdigest(::File.dirname(full))
12
+ base = ::File.basename(file)
13
+
14
+ @config.mkpath(::File.join(@config.cache_root, classdir), base)
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
1
+ module Piggly
2
+ module Util
3
+ module Enumerable
4
+
5
+ # Count number of elements, optionally filtered by a block
6
+ def self.count(enum)
7
+ if block_given?
8
+ enum.inject(0){|count,e| yield(e) ? count + 1 : count }
9
+ else
10
+ enum.length
11
+ end
12
+ end
13
+
14
+ # Compute sum of elements, optionally transformed by a block
15
+ def self.sum(enum, default = 0, &block)
16
+ enum = enum.to_a
17
+ return default if enum.empty?
18
+
19
+ head, *tail = enum
20
+
21
+ if block_given?
22
+ tail.inject(yield(head)){|sum,e| sum + yield(e) }
23
+ else
24
+ tail.inject(head){|sum,e| sum + e }
25
+ end
26
+ end
27
+
28
+ # Collect an elements into disjoint sets, grouped by result of the block
29
+ def self.group_by(enum, collection = Hash.new{|h,k| h[k] = [] })
30
+ enum.inject(collection) do |hash, item|
31
+ hash[yield(item)] << item
32
+ hash
33
+ end
34
+ end
35
+
36
+ def self.index_by(enum, collection = Hash.new)
37
+ enum.inject(collection) do |hash, item|
38
+ hash.update(yield(item) => item)
39
+ end
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,17 @@
1
+ module Piggly
2
+ module Util
3
+ module File
4
+
5
+ # True if target file is older (by mtime) than any source file
6
+ def self.stale?(target, *sources)
7
+ if ::File.exist?(target)
8
+ oldest = ::File.mtime(target)
9
+ sources.any?{|x| ::File.mtime(x) > oldest }
10
+ else
11
+ true
12
+ end
13
+ end
14
+
15
+ end
16
+ end
17
+ end