tiamat 0.1.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 (43) hide show
  1. data/CHANGES.rdoc +7 -0
  2. data/MANIFEST +42 -0
  3. data/README.rdoc +194 -0
  4. data/Rakefile +19 -0
  5. data/bin/tiamat-server +31 -0
  6. data/devel/jumpstart.rb +987 -0
  7. data/install.rb +2 -0
  8. data/lib/tiamat.rb +43 -0
  9. data/lib/tiamat/autoconfig.rb +8 -0
  10. data/lib/tiamat/child_server.rb +21 -0
  11. data/lib/tiamat/config/ruby_parser.rb +6 -0
  12. data/lib/tiamat/connector.rb +23 -0
  13. data/lib/tiamat/error.rb +24 -0
  14. data/lib/tiamat/farm.rb +32 -0
  15. data/lib/tiamat/local_child_farm.rb +23 -0
  16. data/lib/tiamat/local_child_server.rb +25 -0
  17. data/lib/tiamat/local_child_worker.rb +11 -0
  18. data/lib/tiamat/remote_farm.rb +11 -0
  19. data/lib/tiamat/remote_worker.rb +10 -0
  20. data/lib/tiamat/server.rb +42 -0
  21. data/lib/tiamat/tiamat.rb +49 -0
  22. data/lib/tiamat/tiamat_server.rb +48 -0
  23. data/lib/tiamat/util.rb +37 -0
  24. data/lib/tiamat/version.rb +4 -0
  25. data/lib/tiamat/worker.rb +61 -0
  26. data/spec/connector_spec.rb +11 -0
  27. data/spec/drb_connection_spec.rb +18 -0
  28. data/spec/local_child_farm_spec.rb +29 -0
  29. data/spec/local_child_server_path_spec.rb +37 -0
  30. data/spec/local_child_server_spec.rb +24 -0
  31. data/spec/local_child_worker_spec.rb +140 -0
  32. data/spec/pure_spec.rb +59 -0
  33. data/spec/readme_spec.rb +29 -0
  34. data/spec/remote_farm_spec.rb +36 -0
  35. data/spec/remote_worker_spec.rb +59 -0
  36. data/spec/server_spec.rb +48 -0
  37. data/spec/tiamat_open_local_spec.rb +77 -0
  38. data/spec/tiamat_open_remote_spec.rb +67 -0
  39. data/spec/tiamat_server_spec.rb +51 -0
  40. data/spec/tiamat_spec_base.rb +36 -0
  41. data/spec/util_spec.rb +29 -0
  42. data/spec/worker_spec.rb +19 -0
  43. metadata +209 -0
data/CHANGES.rdoc ADDED
@@ -0,0 +1,7 @@
1
+
2
+ = Tiamat ChangeLog
3
+
4
+ == Version 0.1.0
5
+
6
+ * Initial release.
7
+
data/MANIFEST ADDED
@@ -0,0 +1,42 @@
1
+ CHANGES.rdoc
2
+ MANIFEST
3
+ README.rdoc
4
+ Rakefile
5
+ bin/tiamat-server
6
+ devel/jumpstart.rb
7
+ install.rb
8
+ lib/tiamat.rb
9
+ lib/tiamat/autoconfig.rb
10
+ lib/tiamat/child_server.rb
11
+ lib/tiamat/config/ruby_parser.rb
12
+ lib/tiamat/connector.rb
13
+ lib/tiamat/error.rb
14
+ lib/tiamat/farm.rb
15
+ lib/tiamat/local_child_farm.rb
16
+ lib/tiamat/local_child_server.rb
17
+ lib/tiamat/local_child_worker.rb
18
+ lib/tiamat/remote_farm.rb
19
+ lib/tiamat/remote_worker.rb
20
+ lib/tiamat/server.rb
21
+ lib/tiamat/tiamat.rb
22
+ lib/tiamat/tiamat_server.rb
23
+ lib/tiamat/util.rb
24
+ lib/tiamat/version.rb
25
+ lib/tiamat/worker.rb
26
+ spec/connector_spec.rb
27
+ spec/drb_connection_spec.rb
28
+ spec/local_child_farm_spec.rb
29
+ spec/local_child_server_path_spec.rb
30
+ spec/local_child_server_spec.rb
31
+ spec/local_child_worker_spec.rb
32
+ spec/pure_spec.rb
33
+ spec/readme_spec.rb
34
+ spec/remote_farm_spec.rb
35
+ spec/remote_worker_spec.rb
36
+ spec/server_spec.rb
37
+ spec/tiamat_open_local_spec.rb
38
+ spec/tiamat_open_remote_spec.rb
39
+ spec/tiamat_server_spec.rb
40
+ spec/tiamat_spec_base.rb
41
+ spec/util_spec.rb
42
+ spec/worker_spec.rb
data/README.rdoc ADDED
@@ -0,0 +1,194 @@
1
+
2
+ = Tiamat
3
+
4
+ == Summary
5
+
6
+ Automatic parallelism across multiple cores and machines: a plugin for
7
+ Pure.
8
+
9
+ == Synopsis
10
+
11
+ require 'tiamat/autoconfig'
12
+ require 'benchmark'
13
+
14
+ mod = Pure.define do
15
+ def total(left, right)
16
+ left + right
17
+ end
18
+
19
+ def left
20
+ (1..500_000).inject(0) { |acc, n| acc + n }
21
+ end
22
+
23
+ def right
24
+ (1..500_000).inject(0) { |acc, n| acc + n }
25
+ end
26
+ end
27
+
28
+ # compute using two threads
29
+ puts Benchmark.realtime { mod.compute(2).total } # => 0.4432079792022705
30
+
31
+ # compute using two local Ruby interpreters
32
+ Tiamat.open_local(2) {
33
+ puts Benchmark.realtime { mod.compute.total } # => 0.2420041561126709
34
+ }
35
+
36
+ == Description
37
+
38
+ Tiamat is a worker plugin for the pure functional package
39
+ (Pure[http://purefunctional.rubyforge.org]). It links Ruby
40
+ interpreters together with DRb, forming a back-end for Pure's
41
+ parallelizing engine.
42
+
43
+ Tiamat does not modify any of the standard classes.
44
+
45
+ Tiamat has been tested on MRI 1.8.6, 1.8.7, 1.9.1, 1.9.2, and
46
+ jruby-1.4.
47
+
48
+ == Install
49
+
50
+ % gem install tiamat
51
+
52
+ Or for the (non-gem) .tgz package,
53
+
54
+ % ruby install.rb [--uninstall]
55
+
56
+ == Links
57
+
58
+ * Pure: http://purefunctional.rubyforge.org
59
+
60
+ * Documentation: http://tiamat.rubyforge.org
61
+ * Download: http://rubyforge.org/frs/?group_id=9145
62
+ * Rubyforge home: http://rubyforge.org/projects/tiamat
63
+ * Repository: http://github.com/quix/tiamat
64
+
65
+ == Adding +require+ Paths to Local Servers
66
+
67
+ require 'tiamat/autoconfig'
68
+ require 'pure/dsl'
69
+
70
+ require 'matrix'
71
+ require 'complex'
72
+
73
+ mod = pure do
74
+ def f(x, y)
75
+ x + y
76
+ end
77
+ end
78
+
79
+ a = Matrix[[1, 2], [3, 4]]
80
+ b = Matrix[[1, Complex(0, 1)], [Complex(0, -1), 1]]
81
+
82
+ Tiamat.open_local(2, 'matrix', 'complex') {
83
+ puts mod.compute(:x => a, :y => b).f # => Matrix[[2, 2+1i], [3-1i, 5]]
84
+ }
85
+
86
+ == Running Remote Servers
87
+
88
+ First install Tiamat on the remote machines. To start the server,
89
+
90
+ % tiamat-server [drb address] [compiler name] [requires]
91
+
92
+ For example,
93
+
94
+ % tiamat-server druby://192.168.4.1:27272 Pure::Compiler::RubyParser pure/compiler/ruby_parser matrix complex
95
+
96
+ This will make the Matrix and Complex classes available on the remote
97
+ server.
98
+
99
+ To connect (continuing the example in the synopsis),
100
+
101
+ Tiamat.open_remote('druby://192.168.4.1:27272', 'druby://192.168.4.2:27272') {
102
+ puts Benchmark.realtime { mod.compute.total }
103
+ }
104
+
105
+ == Security
106
+
107
+ Running <tt>tiamat-server</tt> is among the most insecure things you
108
+ can possibly do with a computer. The purpose of
109
+ <tt>tiamat-server</tt> is to execute arbitrary code. Only run it
110
+ inside a secure network or private tunnel.
111
+
112
+ == Configuration
113
+
114
+ Configuration consists of two attributes: <tt>Pure.parser</tt> for
115
+ extracting method definitions and <tt>Tiamat.compiler</tt> for
116
+ converting the extracted data into a callable Ruby object. (Though
117
+ strictly speaking <tt>Tiamat.compiler</tt> is only necessary for
118
+ launching local servers.)
119
+
120
+ <tt>require 'tiamat/autoconfig'</tt> finds the optimal parser-compiler
121
+ pair for the running version of Ruby. A configuration may also be
122
+ chosen manually, e.g. <tt>require 'tiamat/config/ruby_parser'</tt>.
123
+
124
+ == Restrictions
125
+
126
+ RubyParser + Ruby2Ruby is currently the only known toolchain which
127
+ provides the ability to extract code, transform the AST, send it over
128
+ the wire, and reconstruct it on another machine.
129
+
130
+ Therefore the syntax of files which contain +pure+ blocks is limited
131
+ to the syntax supported by RubyParser. Other parser plugins with more
132
+ syntax rules are available but they have no compiler counterpart. See
133
+ the latter part of Pure[http://purefunctional.rubyforge.org] for more
134
+ information.
135
+
136
+ == About DRbConnError
137
+
138
+ require 'tiamat/autoconfig'
139
+ require 'pure/dsl'
140
+
141
+ Tiamat.open_local(2) {
142
+ pure do
143
+ def hello(out)
144
+ out.puts("hello")
145
+ end
146
+ end.compute(:out => STDOUT).hello # => raises DRb::DRbConnError
147
+ }
148
+
149
+ This fails because we are trying to send STDOUT to another Ruby
150
+ interpreter. Some Ruby objects have no Marshal.dump, such as IO
151
+ instances (like STDOUT) and Procs. To compensate, the remote DRb
152
+ server tries to phone home with a proxy object but no local server is
153
+ running.
154
+
155
+ If you are interested in a quick fix, try placing this at the
156
+ beginning of your code:
157
+
158
+ require 'drb'
159
+ DRb.start_service
160
+
161
+ See the DRb documentation for more information.
162
+
163
+ Tiamat does not start a local DRb service by default because the
164
+ passing of undumpable arguments to a pure function is likely to be an
165
+ error, as the generated proxies are not thread-safe.
166
+
167
+ == Author
168
+
169
+ * James M. Lawrence <quixoticsycophant@gmail.com>
170
+
171
+ == License
172
+
173
+ Copyright (c) 2009 James M. Lawrence. All rights reserved.
174
+
175
+ Permission is hereby granted, free of charge, to any person
176
+ obtaining a copy of this software and associated documentation files
177
+ (the "Software"), to deal in the Software without restriction,
178
+ including without limitation the rights to use, copy, modify, merge,
179
+ publish, distribute, sublicense, and/or sell copies of the Software,
180
+ and to permit persons to whom the Software is furnished to do so,
181
+ subject to the following conditions:
182
+
183
+ The above copyright notice and this permission notice shall be
184
+ included in all copies or substantial portions of the Software.
185
+
186
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
187
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
188
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
189
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
190
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
191
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
192
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
193
+ SOFTWARE.
194
+
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.unshift "devel"
2
+
3
+ require "jumpstart"
4
+
5
+ Jumpstart.new "tiamat" do |s|
6
+ s.developer("James M. Lawrence", "quixoticsycophant@gmail.com")
7
+ s.rubyforge_user = "quix"
8
+ s.extra_deps = [
9
+ ["pure", ">= 0.2.0"],
10
+ ["ruby_parser", ">= 2.0.4"],
11
+ ["ruby2ruby", ">= 1.2.4"],
12
+ ]
13
+ s.rdoc_files = %w[
14
+ lib/tiamat/tiamat.rb
15
+ lib/tiamat/autoconfig.rb
16
+ ]
17
+ s.description_sentences = 2
18
+ end
19
+
data/bin/tiamat-server ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Search for tiamat installation in this order:
4
+ # * available already
5
+ # * available after rubygems required
6
+ # * avaliable through relative path to this file
7
+ #
8
+
9
+ require 'thread'
10
+ require 'drb'
11
+
12
+ lambda {
13
+ req = lambda {
14
+ require 'tiamat/version'
15
+ require 'tiamat/error'
16
+ require 'tiamat/tiamat_server'
17
+ }
18
+ begin
19
+ req.call
20
+ rescue LoadError
21
+ require 'rubygems'
22
+ begin
23
+ req.call
24
+ rescue LoadError
25
+ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
26
+ req.call
27
+ end
28
+ end
29
+ }.call
30
+
31
+ Tiamat::TiamatServer.run(*ARGV)
@@ -0,0 +1,987 @@
1
+
2
+ class Jumpstart
3
+ class SimpleInstaller
4
+ def initialize
5
+ require 'fileutils'
6
+ require 'rbconfig'
7
+ require 'find'
8
+ dest_root = Config::CONFIG["sitelibdir"]
9
+ sources = []
10
+ Find.find("./lib") { |source|
11
+ if install_file?(source)
12
+ sources << source
13
+ end
14
+ }
15
+ @spec = sources.inject(Array.new) { |acc, source|
16
+ if source == "./lib"
17
+ acc
18
+ else
19
+ dest = File.join(dest_root, source.sub(%r!\A\./lib!, ""))
20
+
21
+ install = lambda {
22
+ if File.directory?(source)
23
+ unless File.directory?(dest)
24
+ puts "mkdir #{dest}"
25
+ FileUtils.mkdir(dest)
26
+ end
27
+ else
28
+ puts "install #{source} --> #{dest}"
29
+ FileUtils.install(source, dest)
30
+ end
31
+ }
32
+
33
+ uninstall = lambda {
34
+ if File.directory?(source)
35
+ if File.directory?(dest)
36
+ puts "rmdir #{dest}"
37
+ FileUtils.rmdir(dest)
38
+ end
39
+ else
40
+ if File.file?(dest)
41
+ puts "rm #{dest}"
42
+ FileUtils.rm(dest)
43
+ end
44
+ end
45
+ }
46
+
47
+ acc << {
48
+ :source => source,
49
+ :dest => dest,
50
+ :install => install,
51
+ :uninstall => uninstall,
52
+ }
53
+ end
54
+ }
55
+ end
56
+
57
+ def install_file?(source)
58
+ File.directory?(source) or
59
+ (File.file?(source) and File.extname(source) == ".rb")
60
+ end
61
+
62
+ def install
63
+ @spec.each { |entry|
64
+ entry[:install].call
65
+ }
66
+ end
67
+
68
+ def uninstall
69
+ @spec.reverse.each { |entry|
70
+ entry[:uninstall].call
71
+ }
72
+ end
73
+
74
+ def run(args = ARGV)
75
+ if args.empty?
76
+ install
77
+ elsif args.size == 1 and args.first == "--uninstall"
78
+ uninstall
79
+ else
80
+ raise "unrecognized arguments: #{args.inspect}"
81
+ end
82
+ end
83
+ end
84
+
85
+ module AttrLazy
86
+ def attr_lazy(name, &block)
87
+ AttrLazy.define_reader(class << self ; self ; end, name, &block)
88
+ end
89
+
90
+ def attr_lazy_accessor(name, &block)
91
+ attr_lazy(name, &block)
92
+ AttrLazy.define_writer(class << self ; self ; end, name, &block)
93
+ end
94
+
95
+ class << self
96
+ def included(mod)
97
+ (class << mod ; self ; end).class_eval do
98
+ def attr_lazy(name, &block)
99
+ AttrLazy.define_reader(self, name, &block)
100
+ end
101
+
102
+ def attr_lazy_accessor(name, &block)
103
+ attr_lazy(name, &block)
104
+ AttrLazy.define_writer(self, name, &block)
105
+ end
106
+ end
107
+ end
108
+
109
+ def define_evaluated_reader(instance, name, value)
110
+ (class << instance ; self ; end).class_eval do
111
+ remove_method name rescue nil
112
+ define_method name do
113
+ value
114
+ end
115
+ end
116
+ end
117
+
118
+ def define_reader(klass, name, &block)
119
+ klass.class_eval do
120
+ remove_method name rescue nil
121
+ define_method name do
122
+ value = instance_eval(&block)
123
+ AttrLazy.define_evaluated_reader(self, name, value)
124
+ value
125
+ end
126
+ end
127
+ end
128
+
129
+ def define_writer(klass, name, &block)
130
+ klass.class_eval do
131
+ writer = "#{name}="
132
+ remove_method writer rescue nil
133
+ define_method writer do |value|
134
+ AttrLazy.define_evaluated_reader(self, name, value)
135
+ value
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ module Ruby
143
+ module_function
144
+
145
+ def executable
146
+ require 'rbconfig'
147
+
148
+ name = File.join(
149
+ Config::CONFIG["bindir"],
150
+ Config::CONFIG["RUBY_INSTALL_NAME"]
151
+ )
152
+
153
+ if Config::CONFIG["host"] =~ %r!(mswin|cygwin|mingw)! and
154
+ File.basename(name) !~ %r!\.(exe|com|bat|cmd)\Z!i
155
+ name + Config::CONFIG["EXEEXT"]
156
+ else
157
+ name
158
+ end
159
+ end
160
+
161
+ def run(*args)
162
+ cmd = [executable, *args]
163
+ unless system(*cmd)
164
+ cmd_str = cmd.map { |t| "'#{t}'" }.join(", ")
165
+ raise "system(#{cmd_str}) failed with status #{$?.exitstatus}"
166
+ end
167
+ end
168
+
169
+ def run_code_and_capture(code)
170
+ IO.popen(%{"#{executable}"}, "r+") { |pipe|
171
+ pipe.print(code)
172
+ pipe.flush
173
+ pipe.close_write
174
+ pipe.read
175
+ }
176
+ end
177
+
178
+ def run_file_and_capture(file)
179
+ unless File.file? file
180
+ raise "file does not exist: `#{file}'"
181
+ end
182
+ IO.popen(%{"#{executable}" "#{file}"}, "r") { |pipe|
183
+ pipe.read
184
+ }
185
+ end
186
+
187
+ def with_warnings(value = true)
188
+ previous = $VERBOSE
189
+ $VERBOSE = value
190
+ begin
191
+ yield
192
+ ensure
193
+ $VERBOSE = previous
194
+ end
195
+ end
196
+
197
+ def no_warnings(&block)
198
+ with_warnings(nil, &block)
199
+ end
200
+ end
201
+
202
+ module Util
203
+ module_function
204
+
205
+ def run_ruby_on_each(*files)
206
+ files.each { |file|
207
+ Ruby.run("-w", file)
208
+ }
209
+ end
210
+
211
+ def to_camel_case(str)
212
+ str.split('_').map { |t| t.capitalize }.join
213
+ end
214
+
215
+ def write_file(file)
216
+ contents = yield
217
+ File.open(file, "wb") { |out|
218
+ out.print(contents)
219
+ }
220
+ contents
221
+ end
222
+
223
+ def replace_file(file)
224
+ old_contents = File.read(file)
225
+ new_contents = yield(old_contents)
226
+ if old_contents != new_contents
227
+ File.open(file, "wb") { |output|
228
+ output.print(new_contents)
229
+ }
230
+ end
231
+ new_contents
232
+ end
233
+ end
234
+
235
+ module InstanceEvalWithArgs
236
+ module_function
237
+
238
+ def with_temp_method(instance, method_name, method_block)
239
+ (class << instance ; self ; end).class_eval do
240
+ define_method(method_name, &method_block)
241
+ begin
242
+ yield method_name
243
+ ensure
244
+ remove_method(method_name)
245
+ end
246
+ end
247
+ end
248
+
249
+ def call_temp_method(instance, method_name, *args, &method_block)
250
+ with_temp_method(instance, method_name, method_block) {
251
+ instance.send(method_name, *args)
252
+ }
253
+ end
254
+
255
+ def instance_eval_with_args(instance, *args, &block)
256
+ call_temp_method(instance, :__temp_method, *args, &block)
257
+ end
258
+ end
259
+
260
+ include AttrLazy
261
+ include Util
262
+
263
+ def initialize(project_name)
264
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
265
+ $LOAD_PATH.unshift File.dirname(__FILE__)
266
+
267
+ require 'rake/gempackagetask'
268
+ require 'rake/clean'
269
+
270
+ @project_name = project_name
271
+
272
+ yield self
273
+
274
+ self.class.instance_methods(false).select { |t|
275
+ t.to_s =~ %r!\Adefine_!
276
+ }.each { |method_name|
277
+ send(method_name)
278
+ }
279
+ end
280
+
281
+ class << self
282
+ alias_method :attribute, :attr_lazy_accessor
283
+ end
284
+
285
+ attribute :name do
286
+ @project_name
287
+ end
288
+
289
+ attribute :version_constant_name do
290
+ "VERSION"
291
+ end
292
+
293
+ attribute :version do
294
+ require name
295
+ mod_name = to_camel_case(name)
296
+ begin
297
+ mod = Kernel.const_get(mod_name)
298
+ if mod.constants.include?(version_constant_name)
299
+ mod.const_get(version_constant_name)
300
+ else
301
+ raise
302
+ end
303
+ rescue
304
+ "0.0.0"
305
+ end
306
+ end
307
+
308
+ attribute :rubyforge_name do
309
+ name.gsub('_', '')
310
+ end
311
+
312
+ attribute :rubyforge_user do
313
+ email.first[%r!^.*(?=@)!]
314
+ end
315
+
316
+ attribute :readme_file do
317
+ "README.rdoc"
318
+ end
319
+
320
+ attribute :history_file do
321
+ "CHANGES.rdoc"
322
+ end
323
+
324
+ attribute :doc_dir do
325
+ "documentation"
326
+ end
327
+
328
+ attribute :spec_files do
329
+ Dir["./spec/*_{spec,example}.rb"]
330
+ end
331
+
332
+ attribute :test_files do
333
+ (Dir["./test/test_*.rb"] + Dir["./test/*_test.rb"]).uniq
334
+ end
335
+
336
+ attribute :rcov_dir do
337
+ "coverage"
338
+ end
339
+
340
+ attribute :spec_output_dir do
341
+ "rspec_output"
342
+ end
343
+
344
+ attribute :spec_output_file do
345
+ "spec.html"
346
+ end
347
+
348
+ attr_lazy :spec_output do
349
+ "#{spec_output_dir}/#{spec_output_file}"
350
+ end
351
+
352
+ [:gem, :tgz].each { |ext|
353
+ attribute ext do
354
+ "pkg/#{name}-#{version}.#{ext}"
355
+ end
356
+ }
357
+
358
+ attribute :rcov_options do
359
+ # workaround for the default rspec task
360
+ Dir["*"].select { |f| File.directory? f }.inject(Array.new) { |acc, dir|
361
+ if dir == "lib"
362
+ acc
363
+ else
364
+ acc + ["--exclude", dir + "/"]
365
+ end
366
+ } + ["--text-report"]
367
+ end
368
+
369
+ attribute :readme_file do
370
+ "README.rdoc"
371
+ end
372
+
373
+ attribute :manifest_file do
374
+ "MANIFEST"
375
+ end
376
+
377
+ attribute :generated_files do
378
+ []
379
+ end
380
+
381
+ attribute :files do
382
+ if File.exist?(manifest_file)
383
+ File.read(manifest_file).split("\n")
384
+ else
385
+ `git ls-files`.split("\n") + [manifest_file] + generated_files
386
+ end
387
+ end
388
+
389
+ attribute :rdoc_files do
390
+ Dir["lib/**/*.rb"]
391
+ end
392
+
393
+ attribute :extra_rdoc_files do
394
+ if File.exist?(readme_file)
395
+ [readme_file]
396
+ else
397
+ []
398
+ end
399
+ end
400
+
401
+ attribute :rdoc_options do
402
+ if File.exist?(readme_file)
403
+ ["--main", readme_file]
404
+ else
405
+ []
406
+ end + [
407
+ "--title", "#{name}: #{summary}",
408
+ ] + (files - rdoc_files).inject(Array.new) { |acc, file|
409
+ acc + ["--exclude", file]
410
+ }
411
+ end
412
+
413
+ attribute :browser do
414
+ require 'rbconfig'
415
+ if Config::CONFIG["host"] =~ %r!darwin!
416
+ app = %w[Firefox Safari].map { |t|
417
+ "/Applications/#{t}.app"
418
+ }.select { |t|
419
+ File.exist? t
420
+ }.first
421
+ if app
422
+ ["open", app]
423
+ else
424
+ raise "need to set `browser'"
425
+ end
426
+ else
427
+ "firefox"
428
+ end
429
+ end
430
+
431
+ attribute :gemspec do
432
+ Gem::Specification.new { |g|
433
+ g.has_rdoc = true
434
+ %w[
435
+ name
436
+ authors
437
+ email
438
+ summary
439
+ version
440
+ description
441
+ files
442
+ extra_rdoc_files
443
+ rdoc_options
444
+ ].each { |param|
445
+ value = send(param) and (
446
+ g.send("#{param}=", value)
447
+ )
448
+ }
449
+
450
+ if rubyforge_name
451
+ g.rubyforge_project = rubyforge_name
452
+ end
453
+
454
+ if url
455
+ g.homepage = url
456
+ end
457
+
458
+ extra_deps.each { |dep|
459
+ g.add_dependency(*dep)
460
+ }
461
+
462
+ extra_dev_deps.each { |dep|
463
+ g.add_development_dependency(*dep)
464
+ }
465
+ }
466
+ end
467
+
468
+ attribute :readme_contents do
469
+ File.read(readme_file) rescue "FIXME: readme_file"
470
+ end
471
+
472
+ attribute :sections do
473
+ require 'enumerator'
474
+ begin
475
+ data = readme_contents.split(%r!^==\s*(.*?)\s*$!)
476
+ pairs = data[1..-1].enum_slice(2).map { |section, contents|
477
+ [section.downcase, contents.strip]
478
+ }
479
+ Hash[*pairs.flatten]
480
+ rescue
481
+ nil
482
+ end
483
+ end
484
+
485
+ attribute :description_section do
486
+ "description"
487
+ end
488
+
489
+ attribute :summary_section do
490
+ "summary"
491
+ end
492
+
493
+ attribute :description_sentences do
494
+ 1
495
+ end
496
+
497
+ attribute :summary_sentences do
498
+ 1
499
+ end
500
+
501
+ [:summary, :description].each { |section|
502
+ attribute section do
503
+ begin
504
+ sections[send("#{section}_section")].
505
+ gsub("\n", " ").
506
+ split(%r!\.\s*!m).
507
+ first(send("#{section}_sentences")).
508
+ join(". ") << "."
509
+ rescue
510
+ "FIXME: #{section}"
511
+ end
512
+ end
513
+ }
514
+
515
+ attribute :url do
516
+ begin
517
+ readme_contents.match(%r!^\*.*?(http://\S+)!)[1]
518
+ rescue
519
+ "http://#{rubyforge_name}.rubyforge.org"
520
+ end
521
+ end
522
+
523
+ attribute :extra_deps do
524
+ []
525
+ end
526
+
527
+ attribute :extra_dev_deps do
528
+ []
529
+ end
530
+
531
+ attribute :authors do
532
+ Array.new
533
+ end
534
+
535
+ attribute :email do
536
+ Array.new
537
+ end
538
+
539
+ def developer(name, email)
540
+ authors << name
541
+ self.email << email
542
+ end
543
+
544
+ def dependency(name, version)
545
+ extra_deps << [name, version]
546
+ end
547
+
548
+ def define_clean
549
+ task :clean do
550
+ Rake::Task[:clobber].invoke
551
+ end
552
+ end
553
+
554
+ def define_package
555
+ task manifest_file do
556
+ create_manifest
557
+ end
558
+ CLEAN.include manifest_file
559
+ task :package => :clean
560
+ Rake::GemPackageTask.new(gemspec) { |t|
561
+ t.need_tar = true
562
+ }
563
+ end
564
+
565
+ def define_spec
566
+ unless spec_files.empty?
567
+ Ruby.no_warnings {
568
+ require 'spec/rake/spectask'
569
+ }
570
+
571
+ desc "run specs"
572
+ Spec::Rake::SpecTask.new('spec') do |t|
573
+ t.spec_files = spec_files
574
+ end
575
+
576
+ desc "run specs with text output"
577
+ Spec::Rake::SpecTask.new('text_spec') do |t|
578
+ t.spec_files = spec_files
579
+ t.spec_opts = ['-fs']
580
+ end
581
+
582
+ desc "run specs with html output"
583
+ Spec::Rake::SpecTask.new('full_spec') do |t|
584
+ t.spec_files = spec_files
585
+ t.rcov = true
586
+ t.rcov_opts = rcov_options
587
+ t.spec_opts = ["-fh:#{spec_output}"]
588
+ end
589
+
590
+ suppress_task_warnings :spec, :full_spec, :text_spec
591
+
592
+ desc "run full_spec then open browser"
593
+ task :show_spec => :full_spec do
594
+ open_browser(spec_output, rcov_dir + "/index.html")
595
+ end
596
+
597
+ desc "run specs individually"
598
+ task :spec_deps do
599
+ run_ruby_on_each(*spec_files)
600
+ end
601
+
602
+ task :prerelease => [:spec, :spec_deps]
603
+ task :default => :spec
604
+
605
+ CLEAN.include spec_output_dir
606
+ end
607
+ end
608
+
609
+ def define_test
610
+ unless test_files.empty?
611
+ desc "run tests"
612
+ task :test do
613
+ test_files.each { |file|
614
+ require file
615
+ }
616
+ end
617
+
618
+ desc "run tests with rcov"
619
+ task :full_test do
620
+ verbose(false) {
621
+ sh("rcov", "-o", rcov_dir, "--text-report",
622
+ *(test_files + rcov_options)
623
+ )
624
+ }
625
+ end
626
+
627
+ desc "run full_test then open browser"
628
+ task :show_test => :full_test do
629
+ open_browser(rcov_dir + "/index.html")
630
+ end
631
+
632
+ desc "run tests individually"
633
+ task :test_deps do
634
+ run_ruby_on_each(*test_files)
635
+ end
636
+
637
+ task :prerelease => [:test, :test_deps]
638
+ task :default => :test
639
+
640
+ CLEAN.include rcov_dir
641
+ end
642
+ end
643
+
644
+ def define_doc
645
+ desc "run rdoc"
646
+ task :doc => :clean_doc do
647
+ require 'rdoc/rdoc'
648
+ args = (
649
+ gemspec.rdoc_options +
650
+ gemspec.require_paths.clone +
651
+ gemspec.extra_rdoc_files +
652
+ ["-o", doc_dir]
653
+ ).flatten.map { |t| t.to_s }
654
+ RDoc::RDoc.new.document args
655
+ end
656
+
657
+ task :clean_doc do
658
+ # normally rm_rf, but mimic rake/clean output
659
+ rm_r(doc_dir) rescue nil
660
+ end
661
+
662
+ desc "run rdoc then open browser"
663
+ task :show_doc => :doc do
664
+ open_browser(doc_dir + "/index.html")
665
+ end
666
+
667
+ task :rdoc => :doc
668
+ task :clean => :clean_doc
669
+ end
670
+
671
+ def define_publish
672
+ desc "upload docs"
673
+ task :publish => [:clean_doc, :doc] do
674
+ require 'rake/contrib/sshpublisher'
675
+ Rake::SshDirPublisher.new(
676
+ "#{rubyforge_user}@rubyforge.org",
677
+ "/var/www/gforge-projects/#{rubyforge_name}",
678
+ doc_dir
679
+ ).upload
680
+ end
681
+ end
682
+
683
+ def define_install
684
+ desc "direct install (no gem)"
685
+ task :install do
686
+ SimpleInstaller.new.run([])
687
+ end
688
+
689
+ desc "direct uninstall (no gem)"
690
+ task :uninstall do
691
+ SimpleInstaller.new.run(["--uninstall"])
692
+ end
693
+ end
694
+
695
+ def define_debug
696
+ runner = Class.new do
697
+ def comment_src_dst(on)
698
+ on ? ["", "#"] : ["#", ""]
699
+ end
700
+
701
+ def comment_regions(on, contents, start)
702
+ src, dst = comment_src_dst(on)
703
+ contents.gsub(%r!^(\s+)#{src}#{start}.*?^\1#{src}(\}|end)!m) { |chunk|
704
+ indent = $1
705
+ chunk.gsub(%r!^#{indent}#{src}!, "#{indent}#{dst}")
706
+ }
707
+ end
708
+
709
+ def comment_lines(on, contents, start)
710
+ src, dst = comment_src_dst(on)
711
+ contents.gsub(%r!^(\s*)#{src}#{start}!) {
712
+ $1 + dst + start
713
+ }
714
+ end
715
+
716
+ def debug_info(enable)
717
+ require 'find'
718
+ Find.find("lib", "test") { |path|
719
+ if path =~ %r!\.rb\Z!
720
+ replace_file(path) { |contents|
721
+ result = comment_regions(!enable, contents, "debug")
722
+ comment_lines(!enable, result, "trace")
723
+ }
724
+ end
725
+ }
726
+ end
727
+ end
728
+
729
+ desc "enable debug and trace calls"
730
+ task :debug_on do
731
+ runner.new.debug_info(true)
732
+ end
733
+
734
+ desc "disable debug and trace calls"
735
+ task :debug_off do
736
+ runner.new.debug_info(false)
737
+ end
738
+ end
739
+
740
+ def define_columns
741
+ desc "check for columns > 80"
742
+ task :check_columns do
743
+ Dir["**/*.rb"].each { |file|
744
+ File.read(file).scan(%r!^.{81}!) { |match|
745
+ unless match =~ %r!http://!
746
+ raise "#{file} greater than 80 columns: #{match}"
747
+ end
748
+ }
749
+ }
750
+ end
751
+ task :prerelease => :check_columns
752
+ end
753
+
754
+ def define_comments
755
+ task :comments do
756
+ file = "comments.txt"
757
+ write_file(file) {
758
+ result = Array.new
759
+ (["Rakefile"] + Dir["**/*.{rb,rake}"]).each { |f|
760
+ File.read(f).scan(%r!\#[^\{].*$!) { |match|
761
+ result << match
762
+ }
763
+ }
764
+ result.join("\n")
765
+ }
766
+ CLEAN.include file
767
+ end
768
+ end
769
+
770
+ def define_check_directory
771
+ task :check_directory do
772
+ unless `git status` =~ %r!nothing to commit \(working directory clean\)!
773
+ raise "Directory not clean"
774
+ end
775
+ end
776
+ end
777
+
778
+ def define_ping
779
+ task :ping do
780
+ require 'rbconfig'
781
+ %w[github.com rubyforge.org].each { |server|
782
+ cmd = "ping " + (
783
+ if Config::CONFIG["host"] =~ %r!darwin!
784
+ "-c2 #{server}"
785
+ else
786
+ "#{server} 2 2"
787
+ end
788
+ )
789
+ unless `#{cmd}` =~ %r!0% packet loss!
790
+ raise "No ping for #{server}"
791
+ end
792
+ }
793
+ end
794
+ end
795
+
796
+ def define_update_jumpstart
797
+ url = ENV["RUBY_JUMPSTART"] || "git://github.com/quix/jumpstart.git"
798
+ task :update_jumpstart do
799
+ git "clone", url
800
+ rm_rf "devel/jumpstart"
801
+ Dir["jumpstart/**/*.rb"].each { |source|
802
+ dest = source.sub(%r!\Ajumpstart/!, "devel/")
803
+ dest_dir = File.dirname(dest)
804
+ mkdir_p(dest_dir) unless File.directory?(dest_dir)
805
+ cp source, dest
806
+ }
807
+ rm_r "jumpstart"
808
+ git "commit", "devel", "-m", "update jumpstart"
809
+ end
810
+ end
811
+
812
+ def git(*args)
813
+ sh("git", *args)
814
+ end
815
+
816
+ def create_manifest
817
+ write_file(manifest_file) {
818
+ files.sort.join("\n")
819
+ }
820
+ end
821
+
822
+ def rubyforge(mode, file, *options)
823
+ command = ["rubyforge", mode] + options + [
824
+ rubyforge_name,
825
+ rubyforge_name,
826
+ version.to_s,
827
+ file,
828
+ ]
829
+ sh(*command)
830
+ end
831
+
832
+ def define_release
833
+ task :prerelease => [:clean, :check_directory, :ping, history_file]
834
+
835
+ task :finish_release do
836
+ gem_md5, tgz_md5 = [gem, tgz].map { |file|
837
+ md5 = "#{file}.md5"
838
+ sh("md5sum #{file} > #{md5}")
839
+ md5
840
+ }
841
+
842
+ rubyforge(
843
+ "add_release", gem, "--release_changes", history_file, "--preformatted"
844
+ )
845
+ [gem_md5, tgz, tgz_md5].each { |file|
846
+ rubyforge("add_file", file)
847
+ }
848
+
849
+ git("tag", "#{name}-" + version.to_s)
850
+ git(*%w(push --tags origin master))
851
+ end
852
+
853
+ task :release => [:prerelease, :package, :publish, :finish_release]
854
+ end
855
+
856
+ def define_debug_gem
857
+ task :debug_gem do
858
+ puts gemspec.to_ruby
859
+ end
860
+ end
861
+
862
+ def open_browser(*files)
863
+ sh(*([browser].flatten + files))
864
+ end
865
+
866
+ def suppress_task_warnings(*task_names)
867
+ task_names.each { |task_name|
868
+ Rake::Task[task_name].actions.map! { |action|
869
+ lambda { |*args|
870
+ Ruby.no_warnings {
871
+ action.call(*args)
872
+ }
873
+ }
874
+ }
875
+ }
876
+ end
877
+
878
+ class << self
879
+ include Util
880
+ include InstanceEvalWithArgs
881
+
882
+ # From minitest, part of the Ruby source; by Ryan Davis.
883
+ def capture_io
884
+ require 'stringio'
885
+
886
+ orig_stdout, orig_stderr = $stdout, $stderr
887
+ captured_stdout, captured_stderr = StringIO.new, StringIO.new
888
+ $stdout, $stderr = captured_stdout, captured_stderr
889
+
890
+ yield
891
+
892
+ return captured_stdout.string, captured_stderr.string
893
+ ensure
894
+ $stdout = orig_stdout
895
+ $stderr = orig_stderr
896
+ end
897
+
898
+ def run_doc_code(code, expected, index, instance, &block)
899
+ lib = File.expand_path(File.dirname(__FILE__) + "/../lib")
900
+ header = %{
901
+ $LOAD_PATH.unshift "#{lib}"
902
+ begin
903
+ }
904
+ footer = %{
905
+ rescue Exception => __jumpstart_exception
906
+ puts "raises \#{__jumpstart_exception.class}"
907
+ end
908
+ }
909
+ final_code = header + code + footer
910
+
911
+ # Sometimes code is required to be inside a file.
912
+ actual = nil
913
+ require 'tempfile'
914
+ Tempfile.open("run-rdoc-code") { |temp_file|
915
+ temp_file.print(final_code)
916
+ temp_file.close
917
+ actual = Ruby.run_file_and_capture(temp_file.path).chomp
918
+ }
919
+
920
+ instance_eval_with_args(instance, expected, actual, index, &block)
921
+ end
922
+
923
+ def run_doc_section(file, section, instance, &block)
924
+ contents = File.read(file)
925
+ re = %r!^=+[ \t]#{Regexp.quote(section)}.*?\n(.*?)^=!m
926
+ if section_contents = contents[re, 1]
927
+ index = 0
928
+ section_contents.scan(%r!^( \S.*?)(?=(^\S|\Z))!m) { |indented, unused|
929
+ code_sections = indented.split(%r!^ \#\#\#\# output:\s*$!)
930
+ code, expected = (
931
+ case code_sections.size
932
+ when 1
933
+ [indented, indented.scan(%r!\# => (.*?)\n!).flatten.join("\n")]
934
+ when 2
935
+ code_sections
936
+ else
937
+ raise "parse error"
938
+ end
939
+ )
940
+ run_doc_code(code, expected, index, instance, &block)
941
+ index += 1
942
+ }
943
+ else
944
+ raise "couldn't find section `#{section}' of `#{file}'"
945
+ end
946
+ end
947
+
948
+ def doc_to_spec(file, *sections, &block)
949
+ jump = self
950
+ describe file do
951
+ sections.each { |section|
952
+ describe "section `#{section}'" do
953
+ it "should run as claimed" do
954
+ if block
955
+ jump.run_doc_section(file, section, self, &block)
956
+ else
957
+ jump.run_doc_section(file, section, self) {
958
+ |expected, actual, index|
959
+ actual.should == expected
960
+ }
961
+ end
962
+ end
963
+ end
964
+ }
965
+ end
966
+ end
967
+
968
+ def doc_to_test(file, *sections, &block)
969
+ jump = self
970
+ klass = Class.new Test::Unit::TestCase do
971
+ sections.each { |section|
972
+ define_method "test_#{file}_#{section}" do
973
+ if block
974
+ jump.run_doc_section(file, section, self, &block)
975
+ else
976
+ jump.run_doc_section(file, section, self) {
977
+ |expected, actual, index|
978
+ assert_equal expected, actual
979
+ }
980
+ end
981
+ end
982
+ }
983
+ end
984
+ Object.const_set("Test#{file}".gsub(".", ""), klass)
985
+ end
986
+ end
987
+ end