cond 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/CHANGES.rdoc +12 -0
  2. data/MANIFEST +39 -0
  3. data/{README → README.rdoc} +23 -28
  4. data/Rakefile +23 -180
  5. data/devel/jumpstart.rb +970 -0
  6. data/install.rb +2 -3
  7. data/lib/cond.rb +38 -453
  8. data/lib/cond/code_section.rb +51 -0
  9. data/lib/cond/cond.rb +159 -0
  10. data/lib/cond/defaults.rb +73 -0
  11. data/lib/cond/dsl.rb +2 -0
  12. data/lib/cond/dsl_definition.rb +74 -0
  13. data/lib/cond/error.rb +17 -0
  14. data/lib/cond/handler.rb +10 -0
  15. data/lib/cond/handling_section.rb +12 -0
  16. data/lib/cond/kernel_raise.rb +59 -0
  17. data/lib/cond/message_proc.rb +15 -0
  18. data/lib/cond/restart.rb +10 -0
  19. data/lib/cond/restartable_section.rb +12 -0
  20. data/lib/cond/symbol_generator.rb +41 -0
  21. data/lib/cond/thread_local.rb +71 -0
  22. data/lib/cond/wrapping.rb +45 -0
  23. data/readmes/restarts.rb +1 -2
  24. data/readmes/seibel_pcl.rb +1 -2
  25. data/{examples/bad_example.rb → spec/bad_spec.rb} +2 -2
  26. data/spec/basic_spec.rb +2 -2
  27. data/{examples/calc_example.rb → spec/calc_spec.rb} +2 -2
  28. data/spec/{common.rb → cond_spec_base.rb} +3 -20
  29. data/spec/error_spec.rb +2 -2
  30. data/spec/leave_again_spec.rb +10 -10
  31. data/spec/matching_spec.rb +1 -1
  32. data/spec/raise_spec.rb +1 -1
  33. data/spec/readme_spec.rb +10 -0
  34. data/spec/reraise_spec.rb +2 -2
  35. data/{examples/restarts_example.rb → spec/restarts_spec.rb} +7 -4
  36. data/{examples/seibel_example.rb → spec/seibel_spec.rb} +4 -6
  37. data/spec/symbols_spec.rb +2 -2
  38. data/spec/thread_local_spec.rb +8 -8
  39. data/spec/wrapping_spec.rb +2 -2
  40. metadata +110 -42
  41. data/cond.gemspec +0 -37
  42. data/examples/readme_example.rb +0 -27
  43. data/lib/cond/cond_private/defaults.rb +0 -78
  44. data/lib/cond/cond_private/symbol_generator.rb +0 -45
  45. data/lib/cond/cond_private/thread_local.rb +0 -77
  46. data/spec/specs_spec.rb +0 -16
  47. data/support/quix/ruby.rb +0 -51
  48. data/support/quix/simple_installer.rb +0 -88
data/CHANGES.rdoc ADDED
@@ -0,0 +1,12 @@
1
+
2
+ = Cond ChangeLog
3
+
4
+ == Version 0.3.0
5
+
6
+ * require 'cond/dsl' to get the DSL; no more 'include Cond'
7
+ * internal cleanup
8
+
9
+ == Version 0.2.1
10
+
11
+ * initial release
12
+
data/MANIFEST ADDED
@@ -0,0 +1,39 @@
1
+ CHANGES.rdoc
2
+ MANIFEST
3
+ README.rdoc
4
+ Rakefile
5
+ devel/jumpstart.rb
6
+ install.rb
7
+ lib/cond.rb
8
+ lib/cond/code_section.rb
9
+ lib/cond/cond.rb
10
+ lib/cond/defaults.rb
11
+ lib/cond/dsl.rb
12
+ lib/cond/dsl_definition.rb
13
+ lib/cond/error.rb
14
+ lib/cond/handler.rb
15
+ lib/cond/handling_section.rb
16
+ lib/cond/kernel_raise.rb
17
+ lib/cond/message_proc.rb
18
+ lib/cond/restart.rb
19
+ lib/cond/restartable_section.rb
20
+ lib/cond/symbol_generator.rb
21
+ lib/cond/thread_local.rb
22
+ lib/cond/wrapping.rb
23
+ readmes/restarts.rb
24
+ readmes/seibel_pcl.rb
25
+ spec/bad_spec.rb
26
+ spec/basic_spec.rb
27
+ spec/calc_spec.rb
28
+ spec/cond_spec_base.rb
29
+ spec/error_spec.rb
30
+ spec/leave_again_spec.rb
31
+ spec/matching_spec.rb
32
+ spec/raise_spec.rb
33
+ spec/readme_spec.rb
34
+ spec/reraise_spec.rb
35
+ spec/restarts_spec.rb
36
+ spec/seibel_spec.rb
37
+ spec/symbols_spec.rb
38
+ spec/thread_local_spec.rb
39
+ spec/wrapping_spec.rb
@@ -1,14 +1,13 @@
1
1
 
2
2
  = Cond
3
3
 
4
- == Description
4
+ == Summary
5
5
 
6
6
  Resolve errors without unwinding the stack.
7
7
 
8
8
  == Synopsis
9
9
 
10
- require 'cond'
11
- include Cond
10
+ require 'cond/dsl'
12
11
 
13
12
  def divide(x, y)
14
13
  restartable do
@@ -36,7 +35,7 @@ Resolve errors without unwinding the stack.
36
35
  or
37
36
  % ruby install.rb [--uninstall]
38
37
 
39
- == Overview
38
+ == Description
40
39
 
41
40
  Cond allows errors to be handled near the place where they occur,
42
41
  before the stack unwinds. It offers several advantages over
@@ -84,9 +83,8 @@ propagate in the usual unwinding fashion, as if the handler was never
84
83
  called.
85
84
 
86
85
  Cond is 100% compatible with the built-in exception-handling system.
87
- We may imagine that Ruby had this handler/restart functionality from
88
- the very beginning, but everyone had forgotten to write restarts. And
89
- since no restarts were available, no handlers were written.
86
+ We may imagine that Ruby had always had this handler+restart
87
+ functionality but nobody remembered to use it.
90
88
 
91
89
  == Background
92
90
 
@@ -105,6 +103,8 @@ http://www.gigamonkeys.com/book/beyond-exception-handling-conditions-and-restart
105
103
  See readmes/seibel_pcl.rb for a Ruby translation.
106
104
 
107
105
  == Synopsis 2.0
106
+
107
+ require 'cond/dsl'
108
108
 
109
109
  x, y = 7, 0
110
110
 
@@ -134,14 +134,10 @@ arbitrarily.
134
134
  for repetitive catch blocks and prevent symbol collisions for nested
135
135
  catch labels.
136
136
 
137
- A default handler is provided which runs a simple choose-a-restart
138
- input loop when +raise+ is called, as the next example demonstrates.
139
-
140
137
  == Restart Example
141
138
 
142
139
  require 'pp'
143
- require 'cond'
144
- include Cond
140
+ require 'cond/dsl'
145
141
 
146
142
  class RestartableFetchError < RuntimeError
147
143
  end
@@ -228,25 +224,24 @@ Cond has been tested on MRI 1.8.6, 1.8.7, 1.9, and the latest jruby.
228
224
  Each thread keeps its own list of handlers, restarts, and other data.
229
225
  All operations are fully thread-safe.
230
226
 
231
- It is not required to <tt>include Cond</tt>. The includable methods
232
- of +Cond+ are <tt>module_function</tt>s and are thus callable via e.g.
233
- <tt>Cond.handling</tt>.
234
-
235
- +Cond+ nests private modules and classes inside Cond::CondPrivate in
236
- order to improve the hygiene of <tt>include Cond</tt> and encourage
237
- its use.
238
-
239
227
  Except for the redefinition +raise+, Cond does not silently modify any
240
- of the standard classes
228
+ of the standard classes.
241
229
 
242
230
  The essential implementation is small and simple: it consists of two
243
231
  per-thread stacks of hashes (handlers and restarts) with merge-push
244
- and pop operations. The syntax shown in the above examples is a thin
245
- layer concealing the underlying hashes. It is equivalent to the
246
- following raw form. You are free to use either form according to
247
- preference or circumstance.
232
+ and pop operations.
233
+
234
+ == DSL Form and Raw Form
235
+
236
+ The optional <tt>require 'cond/dsl'</tt> defines some pseudo-keywords
237
+ in the global scope which comprise a DSL for the system. These
238
+ methods are also available with <tt>require 'cond'</tt> through the
239
+ Cond singleton (e.g. Cond.handling) or by including Cond::DSL into the
240
+ class or module which uses them.
248
241
 
249
- === Raw Form
242
+ The DSL shown in the above examples is a thin layer concealing the
243
+ underlying hashes. It is equivalent to the following raw form. You
244
+ are free to use either form according to preference or circumstance.
250
245
 
251
246
  require 'cond'
252
247
 
@@ -276,10 +271,10 @@ preference or circumstance.
276
271
  puts divide(7, 0) # => 42
277
272
  end
278
273
 
279
- === Limitations
274
+ == Limitations
280
275
 
281
276
  There must be a call to +raise+ inside Ruby code (as opposed to C
282
- code) for a handler to be invoked.
277
+ code) in order for a handler to be invoked.
283
278
 
284
279
  The above synopsis gives an example: Why is there a check for division
285
280
  by zero when +ZeroDivisionError+ would be raised anyway? Because
data/Rakefile CHANGED
@@ -1,59 +1,27 @@
1
-
2
- require 'rake'
3
- require 'spec/rake/spectask'
4
- require 'rake/gempackagetask'
5
- require 'rake/contrib/rubyforgepublisher'
6
- require 'rdoc/rdoc'
7
-
8
- require 'fileutils'
9
- include FileUtils
10
-
11
- README = "README"
12
- PROJECT_NAME = "cond"
13
- GEMSPEC = eval(File.read("#{PROJECT_NAME}.gemspec"))
14
- raise unless GEMSPEC.name == PROJECT_NAME
15
- DOC_DIR = "html"
16
-
17
- SPEC_FILES = Dir['spec/*_spec.rb'] + Dir['examples/*_example.rb']
18
- SPEC_OUTPUT = "spec_output.html"
19
-
20
- ######################################################################
21
- # default
22
-
23
- task :default => :spec
24
-
25
- ######################################################################
26
- # spec
27
-
28
- Spec::Rake::SpecTask.new('spec') do |t|
29
- t.spec_files = SPEC_FILES
30
- end
31
-
32
- Spec::Rake::SpecTask.new('text_spec') do |t|
33
- t.spec_files = SPEC_FILES
34
- t.spec_opts = ['-fs']
35
- end
36
-
37
- Spec::Rake::SpecTask.new('full_spec') do |t|
38
- t.spec_files = SPEC_FILES
39
- t.rcov = true
40
- exclude_dirs = %w[readmes support examples spec]
41
- t.rcov_opts = exclude_dirs.inject(Array.new) { |acc, dir|
42
- acc + ["--exclude", dir]
43
- }
44
- t.spec_opts = ["-fh:#{SPEC_OUTPUT}"]
45
- end
46
-
47
- task :show_full_spec => :full_spec do
48
- args = SPEC_OUTPUT, "coverage/index.html"
49
- open_browser(*args)
1
+ $LOAD_PATH.unshift "devel"
2
+
3
+ require 'jumpstart'
4
+
5
+ readme_file = nil
6
+
7
+ Jumpstart.new('cond') do |s|
8
+ s.developer('James M. Lawrence', 'quixoticsycophant@gmail.com')
9
+ s.rubyforge_user = "quix"
10
+ s.description_sentences = 2
11
+ s.rdoc_files = %w[
12
+ lib/cond/cond.rb
13
+ lib/cond/dsl_definition.rb
14
+ lib/cond/error.rb
15
+ lib/cond/handler.rb
16
+ lib/cond/message_proc.rb
17
+ lib/cond/restart.rb
18
+ lib/cond/wrapping.rb
19
+ ]
20
+ readme_file = s.readme_file
50
21
  end
51
22
 
52
- ######################################################################
53
- # readme
54
-
55
23
  task :readme do
56
- readme = File.read(README)
24
+ readme = File.read(readme_file)
57
25
  restarts = File.read("readmes/restarts.rb")
58
26
  run_re = %r!\A\# !
59
27
  update = readme.sub(%r!(= Restart Example\n)(.*?)(?=^Run)!m) {
@@ -64,132 +32,7 @@ task :readme do
64
32
  $1 + "\n" +
65
33
  restarts.lines.grep(run_re).map { |t| t.sub(run_re, " ") }.join + "\n"
66
34
  }
67
- File.open(README, "w") { |f| f.print update }
68
- end
69
-
70
- ######################################################################
71
- # clean
72
-
73
- task :clean => [:clobber, :clean_doc] do
74
- end
75
-
76
- task :clean_doc do
77
- rm_rf(DOC_DIR)
78
- rm_f(SPEC_OUTPUT)
79
- end
80
-
81
- ######################################################################
82
- # package
83
-
84
- task :package => :clean
85
-
86
- Rake::GemPackageTask.new(GEMSPEC) { |t|
87
- t.need_tar = true
88
- }
89
-
90
- ######################################################################
91
- # doc
92
-
93
- task :doc => :clean_doc do
94
- files = %W[#{README} lib/cond.rb]
95
-
96
- options = [
97
- "-o", DOC_DIR,
98
- "--title", "#{GEMSPEC.name}: #{GEMSPEC.summary}",
99
- "--main", README
100
- ]
101
-
102
- RDoc::RDoc.new.document(files + options)
103
- end
104
-
105
- task :rdoc => :doc
106
-
107
- task :show_doc => :doc do
108
- open_browser("#{DOC_DIR}/index.html")
109
- end
110
-
111
- ######################################################################
112
- # misc
113
-
114
- def open_browser(*files)
115
- if Config::CONFIG["host"] =~ %r!darwin!
116
- sh("open", "/Applications/Firefox.app", *files)
117
- else
118
- sh("firefox", *files)
119
- end
120
- end
121
-
122
- ######################################################################
123
- # git
124
-
125
- def git(*args)
126
- sh("git", *args)
127
- end
128
-
129
- ######################################################################
130
- # publisher
131
-
132
- task :publish => :doc do
133
- Rake::RubyForgePublisher.new(GEMSPEC.name, 'quix').upload
134
- end
135
-
136
- ######################################################################
137
- # release
138
-
139
- unless respond_to? :tap
140
- module Kernel
141
- def tap
142
- yield self
143
- self
144
- end
145
- end
146
- end
147
-
148
- task :prerelease => :clean do
149
- rm_rf(DOC_DIR)
150
- rm_rf("pkg")
151
- unless `git status` =~ %r!nothing to commit \(working directory clean\)!
152
- raise "Directory not clean"
153
- end
154
- unless `ping github.com 2 2` =~ %r!0% packet loss!i
155
- raise "No ping for github.com"
156
- end
157
- end
158
-
159
- def rubyforge(command, file)
160
- sh(
161
- "rubyforge",
162
- command,
163
- GEMSPEC.rubyforge_project,
164
- GEMSPEC.rubyforge_project,
165
- GEMSPEC.version.to_s,
166
- file
167
- )
168
- end
169
-
170
- task :finish_release do
171
- gem, tgz = %w(gem tgz).map { |ext|
172
- "pkg/#{GEMSPEC.name}-#{GEMSPEC.version}.#{ext}"
173
- }
174
- gem_md5, tgz_md5 = [gem, tgz].map { |file|
175
- "#{file}.md5".tap { |md5|
176
- sh("md5sum #{file} > #{md5}")
177
- }
178
- }
179
-
180
- rubyforge("add_release", gem)
181
- rubyforge("add_file", gem_md5)
182
- rubyforge("add_file", tgz)
183
- rubyforge("add_file", tgz_md5)
184
-
185
- git("tag", "#{GEMSPEC.name}-" + GEMSPEC.version.to_s)
186
- git(*%w(push --tags origin master))
35
+ File.open(readme_file, "w") { |f| f.print update }
187
36
  end
188
37
 
189
- task :release =>
190
- [
191
- :prerelease,
192
- :package,
193
- :publish,
194
- :finish_release,
195
- ]
38
+ task :prerelease => :readme
@@ -0,0 +1,970 @@
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
+ begin
474
+ pairs = Hash[*readme_contents.split(%r!^== (\w+).*?$!)[1..-1]].map {
475
+ |section, contents|
476
+ [section.downcase, contents.strip]
477
+ }
478
+ Hash[*pairs.flatten]
479
+ rescue
480
+ nil
481
+ end
482
+ end
483
+
484
+ attribute :description_section do
485
+ "description"
486
+ end
487
+
488
+ attribute :summary_section do
489
+ "summary"
490
+ end
491
+
492
+ attribute :description_sentences do
493
+ 1
494
+ end
495
+
496
+ attribute :summary_sentences do
497
+ 1
498
+ end
499
+
500
+ [:summary, :description].each { |section|
501
+ attribute section do
502
+ begin
503
+ sections[send("#{section}_section")].
504
+ gsub("\n", " ").
505
+ split(%r!\.\s*!m).
506
+ first(send("#{section}_sentences")).
507
+ join(". ") << "."
508
+ rescue
509
+ "FIXME: #{section}"
510
+ end
511
+ end
512
+ }
513
+
514
+ attribute :url do
515
+ begin
516
+ readme_contents.match(%r!^\*.*?(http://\S+)!)[1]
517
+ rescue
518
+ "http://#{rubyforge_name}.rubyforge.org"
519
+ end
520
+ end
521
+
522
+ attribute :extra_deps do
523
+ []
524
+ end
525
+
526
+ attribute :extra_dev_deps do
527
+ []
528
+ end
529
+
530
+ attribute :authors do
531
+ Array.new
532
+ end
533
+
534
+ attribute :email do
535
+ Array.new
536
+ end
537
+
538
+ def developer(name, email)
539
+ authors << name
540
+ self.email << email
541
+ end
542
+
543
+ def dependency(name, version)
544
+ extra_deps << [name, version]
545
+ end
546
+
547
+ def define_clean
548
+ task :clean do
549
+ Rake::Task[:clobber].invoke
550
+ end
551
+ end
552
+
553
+ def define_package
554
+ task manifest_file do
555
+ create_manifest
556
+ end
557
+ CLEAN.include manifest_file
558
+ task :package => :clean
559
+ Rake::GemPackageTask.new(gemspec) { |t|
560
+ t.need_tar = true
561
+ }
562
+ end
563
+
564
+ def define_spec
565
+ unless spec_files.empty?
566
+ require 'spec/rake/spectask'
567
+
568
+ desc "run specs"
569
+ Spec::Rake::SpecTask.new('spec') do |t|
570
+ t.spec_files = spec_files
571
+ end
572
+
573
+ desc "run specs with text output"
574
+ Spec::Rake::SpecTask.new('text_spec') do |t|
575
+ t.spec_files = spec_files
576
+ t.spec_opts = ['-fs']
577
+ end
578
+
579
+ desc "run specs with html output"
580
+ Spec::Rake::SpecTask.new('full_spec') do |t|
581
+ t.spec_files = spec_files
582
+ t.rcov = true
583
+ t.rcov_opts = rcov_options
584
+ t.spec_opts = ["-fh:#{spec_output}"]
585
+ end
586
+
587
+ desc "run full_spec then open browser"
588
+ task :show_spec => :full_spec do
589
+ open_browser(spec_output, rcov_dir + "/index.html")
590
+ end
591
+
592
+ desc "run specs individually"
593
+ task :spec_deps do
594
+ run_ruby_on_each(*spec_files)
595
+ end
596
+
597
+ task :prerelease => [:spec, :spec_deps]
598
+ task :default => :spec
599
+
600
+ CLEAN.include spec_output_dir
601
+ end
602
+ end
603
+
604
+ def define_test
605
+ unless test_files.empty?
606
+ desc "run tests"
607
+ task :test do
608
+ test_files.each { |file|
609
+ require file
610
+ }
611
+ end
612
+
613
+ desc "run tests with rcov"
614
+ task :full_test do
615
+ verbose(false) {
616
+ sh("rcov", "-o", rcov_dir, "--text-report",
617
+ *(test_files + rcov_options)
618
+ )
619
+ }
620
+ end
621
+
622
+ desc "run full_test then open browser"
623
+ task :show_test => :full_test do
624
+ open_browser(rcov_dir + "/index.html")
625
+ end
626
+
627
+ desc "run tests individually"
628
+ task :test_deps do
629
+ run_ruby_on_each(*test_files)
630
+ end
631
+
632
+ task :prerelease => [:test, :test_deps]
633
+ task :default => :test
634
+
635
+ CLEAN.include rcov_dir
636
+ end
637
+ end
638
+
639
+ def define_doc
640
+ desc "run rdoc"
641
+ task :doc => :clean_doc do
642
+ require 'rdoc/rdoc'
643
+ args = (
644
+ gemspec.rdoc_options +
645
+ gemspec.require_paths.clone +
646
+ gemspec.extra_rdoc_files +
647
+ ["-o", doc_dir]
648
+ ).flatten.map { |t| t.to_s }
649
+ RDoc::RDoc.new.document args
650
+ end
651
+
652
+ task :clean_doc do
653
+ # normally rm_rf, but mimic rake/clean output
654
+ rm_r(doc_dir) rescue nil
655
+ end
656
+
657
+ desc "run rdoc then open browser"
658
+ task :show_doc => :doc do
659
+ open_browser(doc_dir + "/index.html")
660
+ end
661
+
662
+ task :rdoc => :doc
663
+ task :clean => :clean_doc
664
+ end
665
+
666
+ def define_publish
667
+ desc "upload docs"
668
+ task :publish => [:clean_doc, :doc] do
669
+ require 'rake/contrib/sshpublisher'
670
+ Rake::SshDirPublisher.new(
671
+ "#{rubyforge_user}@rubyforge.org",
672
+ "/var/www/gforge-projects/#{rubyforge_name}",
673
+ doc_dir
674
+ ).upload
675
+ end
676
+ end
677
+
678
+ def define_install
679
+ desc "direct install (no gem)"
680
+ task :install do
681
+ SimpleInstaller.new.run([])
682
+ end
683
+
684
+ desc "direct uninstall (no gem)"
685
+ task :uninstall do
686
+ SimpleInstaller.new.run(["--uninstall"])
687
+ end
688
+ end
689
+
690
+ def define_debug
691
+ runner = Class.new do
692
+ def comment_src_dst(on)
693
+ on ? ["", "#"] : ["#", ""]
694
+ end
695
+
696
+ def comment_regions(on, contents, start)
697
+ src, dst = comment_src_dst(on)
698
+ contents.gsub(%r!^(\s+)#{src}#{start}.*?^\1#{src}(\}|end)!m) { |chunk|
699
+ indent = $1
700
+ chunk.gsub(%r!^#{indent}#{src}!, "#{indent}#{dst}")
701
+ }
702
+ end
703
+
704
+ def comment_lines(on, contents, start)
705
+ src, dst = comment_src_dst(on)
706
+ contents.gsub(%r!^(\s*)#{src}#{start}!) {
707
+ $1 + dst + start
708
+ }
709
+ end
710
+
711
+ def debug_info(enable)
712
+ require 'find'
713
+ Find.find("lib", "test") { |path|
714
+ if path =~ %r!\.rb\Z!
715
+ replace_file(path) { |contents|
716
+ result = comment_regions(!enable, contents, "debug")
717
+ comment_lines(!enable, result, "trace")
718
+ }
719
+ end
720
+ }
721
+ end
722
+ end
723
+
724
+ desc "enable debug and trace calls"
725
+ task :debug_on do
726
+ runner.new.debug_info(true)
727
+ end
728
+
729
+ desc "disable debug and trace calls"
730
+ task :debug_off do
731
+ runner.new.debug_info(false)
732
+ end
733
+ end
734
+
735
+ def define_columns
736
+ desc "check for columns > 80"
737
+ task :check_columns do
738
+ Dir["**/*.rb"].each { |file|
739
+ File.read(file).scan(%r!^.{81}!) { |match|
740
+ unless match =~ %r!http://!
741
+ raise "#{file} greater than 80 columns: #{match}"
742
+ end
743
+ }
744
+ }
745
+ end
746
+ task :prerelease => :check_columns
747
+ end
748
+
749
+ def define_comments
750
+ task :comments do
751
+ file = "comments.txt"
752
+ write_file(file) {
753
+ result = Array.new
754
+ (["Rakefile"] + Dir["**/*.{rb,rake}"]).each { |f|
755
+ File.read(f).scan(%r!\#[^\{].*$!) { |match|
756
+ result << match
757
+ }
758
+ }
759
+ result.join("\n")
760
+ }
761
+ CLEAN.include file
762
+ end
763
+ end
764
+
765
+ def define_check_directory
766
+ task :check_directory do
767
+ unless `git status` =~ %r!nothing to commit \(working directory clean\)!
768
+ raise "Directory not clean"
769
+ end
770
+ end
771
+ end
772
+
773
+ def define_ping
774
+ task :ping do
775
+ require 'rbconfig'
776
+ %w[github.com rubyforge.org].each { |server|
777
+ cmd = "ping " + (
778
+ if Config::CONFIG["host"] =~ %r!darwin!
779
+ "-c2 #{server}"
780
+ else
781
+ "#{server} 2 2"
782
+ end
783
+ )
784
+ unless `#{cmd}` =~ %r!0% packet loss!
785
+ raise "No ping for #{server}"
786
+ end
787
+ }
788
+ end
789
+ end
790
+
791
+ def define_update_jumpstart
792
+ url = ENV["RUBY_JUMPSTART"] || "git://github.com/quix/jumpstart.git"
793
+ task :update_jumpstart do
794
+ git "clone", url
795
+ rm_rf "devel/jumpstart"
796
+ Dir["jumpstart/**/*.rb"].each { |source|
797
+ dest = source.sub(%r!\Ajumpstart/!, "devel/")
798
+ dest_dir = File.dirname(dest)
799
+ mkdir_p(dest_dir) unless File.directory?(dest_dir)
800
+ cp source, dest
801
+ }
802
+ rm_r "jumpstart"
803
+ git "commit", "devel", "-m", "update jumpstart"
804
+ end
805
+ end
806
+
807
+ def git(*args)
808
+ sh("git", *args)
809
+ end
810
+
811
+ def create_manifest
812
+ write_file(manifest_file) {
813
+ files.sort.join("\n")
814
+ }
815
+ end
816
+
817
+ def rubyforge(mode, file, *options)
818
+ command = ["rubyforge", mode] + options + [
819
+ rubyforge_name,
820
+ rubyforge_name,
821
+ version.to_s,
822
+ file,
823
+ ]
824
+ sh(*command)
825
+ end
826
+
827
+ def define_release
828
+ task :prerelease => [:clean, :check_directory, :ping, history_file]
829
+
830
+ task :finish_release do
831
+ gem_md5, tgz_md5 = [gem, tgz].map { |file|
832
+ md5 = "#{file}.md5"
833
+ sh("md5sum #{file} > #{md5}")
834
+ md5
835
+ }
836
+
837
+ rubyforge(
838
+ "add_release", gem, "--release_changes", history_file, "--preformatted"
839
+ )
840
+ [gem_md5, tgz, tgz_md5].each { |file|
841
+ rubyforge("add_file", file)
842
+ }
843
+
844
+ git("tag", "#{name}-" + version.to_s)
845
+ git(*%w(push --tags origin master))
846
+ end
847
+
848
+ task :release => [:prerelease, :package, :publish, :finish_release]
849
+ end
850
+
851
+ def define_debug_gem
852
+ task :debug_gem do
853
+ puts gemspec.to_ruby
854
+ end
855
+ end
856
+
857
+ def open_browser(*files)
858
+ sh(*([browser].flatten + files))
859
+ end
860
+
861
+ class << self
862
+ include Util
863
+ include InstanceEvalWithArgs
864
+
865
+ # From minitest, part of the Ruby source; by Ryan Davis.
866
+ def capture_io
867
+ require 'stringio'
868
+
869
+ orig_stdout, orig_stderr = $stdout, $stderr
870
+ captured_stdout, captured_stderr = StringIO.new, StringIO.new
871
+ $stdout, $stderr = captured_stdout, captured_stderr
872
+
873
+ yield
874
+
875
+ return captured_stdout.string, captured_stderr.string
876
+ ensure
877
+ $stdout = orig_stdout
878
+ $stderr = orig_stderr
879
+ end
880
+
881
+ def run_doc_code(code, expected, index, instance, &block)
882
+ lib = File.expand_path(File.dirname(__FILE__) + "/../lib")
883
+ header = %{
884
+ $LOAD_PATH.unshift "#{lib}"
885
+ require 'rubygems'
886
+ begin
887
+ }
888
+ footer = %{
889
+ rescue Exception => __jumpstart_exception
890
+ puts "raises \#{__jumpstart_exception.class}"
891
+ end
892
+ }
893
+ final_code = header + code + footer
894
+
895
+ # Sometimes code is required to be inside a file.
896
+ actual = nil
897
+ require 'tempfile'
898
+ Tempfile.open("run-rdoc-code") { |temp_file|
899
+ temp_file.print(final_code)
900
+ temp_file.close
901
+ actual = Ruby.run_file_and_capture(temp_file.path).chomp
902
+ }
903
+
904
+ instance_eval_with_args(instance, expected, actual, index, &block)
905
+ end
906
+
907
+ def run_doc_section(file, section, instance, &block)
908
+ contents = File.read(file)
909
+ if section_contents = contents[%r!^=+[ \t]#{section}.*?\n(.*?)^=!m, 1]
910
+ index = 0
911
+ section_contents.scan(%r!^( \S.*?)(?=(^\S|\Z))!m) { |indented, unused|
912
+ code_sections = indented.split(%r!^ \#\#\#\# output:\s*$!)
913
+ code, expected = (
914
+ case code_sections.size
915
+ when 1
916
+ [indented, indented.scan(%r!\# => (.*?)\n!).flatten.join("\n")]
917
+ when 2
918
+ code_sections
919
+ else
920
+ raise "parse error"
921
+ end
922
+ )
923
+ run_doc_code(code, expected, index, instance, &block)
924
+ index += 1
925
+ }
926
+ else
927
+ raise "couldn't find section `#{section}' of `#{file}'"
928
+ end
929
+ end
930
+
931
+ def doc_to_spec(file, *sections, &block)
932
+ jump = self
933
+ describe file do
934
+ sections.each { |section|
935
+ describe "section `#{section}'" do
936
+ it "should run as claimed" do
937
+ if block
938
+ jump.run_doc_section(file, section, self, &block)
939
+ else
940
+ jump.run_doc_section(file, section, self) {
941
+ |expected, actual, index|
942
+ actual.should == expected
943
+ }
944
+ end
945
+ end
946
+ end
947
+ }
948
+ end
949
+ end
950
+
951
+ def doc_to_test(file, *sections, &block)
952
+ jump = self
953
+ klass = Class.new Test::Unit::TestCase do
954
+ sections.each { |section|
955
+ define_method "test_#{file}_#{section}" do
956
+ if block
957
+ jump.run_doc_section(file, section, self, &block)
958
+ else
959
+ jump.run_doc_section(file, section, self) {
960
+ |expected, actual, index|
961
+ assert_equal expected, actual
962
+ }
963
+ end
964
+ end
965
+ }
966
+ end
967
+ Object.const_set("Test#{file}".gsub(".", ""), klass)
968
+ end
969
+ end
970
+ end