tiamat 0.1.0

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