test-unit-mock 0.30
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.
- data/COPYING +58 -0
- data/ChangeLog +262 -0
- data/README +203 -0
- data/install.rb +85 -0
- data/misc/readmecode.rb +125 -0
- data/mock.rb +467 -0
- data/test-unit-mock.gemspec +20 -0
- data/test.rb +327 -0
- data/utils.rb +345 -0
- metadata +59 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'date'
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = %q{test-unit-mock}
|
4
|
+
s.version = "0.30"
|
5
|
+
s.date = Date.today.to_s
|
6
|
+
s.summary = %q{Test::Unit::Mock is a class for conveniently building mock objects in Test::Unit test cases.}
|
7
|
+
s.author = %q{Michael Granger}
|
8
|
+
s.email = %q{ged@FaerieMUD.org}
|
9
|
+
s.homepage = %q{http://www.deveiate.org/code/Test-Unit-Mock.html}
|
10
|
+
s.files = Dir.glob('**/*')
|
11
|
+
s.require_path = %q{.}
|
12
|
+
s.autorequire = %q{mock}
|
13
|
+
s.has_rdoc = true
|
14
|
+
s.rdoc_options = ["--main", "README"]
|
15
|
+
s.extra_rdoc_files = ["README"]
|
16
|
+
s.test_files = %w{test.rb}
|
17
|
+
s.require_path = %q{.}
|
18
|
+
s.add_dependency('algorithm-diff', '>= 0.0.0')
|
19
|
+
s.required_ruby_version = Gem::Version::Requirement.new(">= 1.6.8")
|
20
|
+
end
|
data/test.rb
ADDED
@@ -0,0 +1,327 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
# :nodoc: all
|
3
|
+
#
|
4
|
+
# Test suite for Test::Unit::MockObject
|
5
|
+
#
|
6
|
+
#
|
7
|
+
|
8
|
+
$LOAD_PATH.unshift '.'
|
9
|
+
|
10
|
+
require 'test/unit'
|
11
|
+
require 'mock'
|
12
|
+
|
13
|
+
require 'utils'
|
14
|
+
include UtilityFunctions
|
15
|
+
|
16
|
+
### Set up a class to be mocked
|
17
|
+
class TestClass
|
18
|
+
def initialize( arg1 )
|
19
|
+
@arg1 = arg1
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_accessor :arg1
|
23
|
+
|
24
|
+
def first
|
25
|
+
return "This is the first method"
|
26
|
+
end
|
27
|
+
|
28
|
+
def second( arg1 )
|
29
|
+
return "This is the second method (#{arg1})"
|
30
|
+
end
|
31
|
+
|
32
|
+
def third( arg1, arg2="default" )
|
33
|
+
return "This is the third method (#{arg1}, #{arg2})"
|
34
|
+
end
|
35
|
+
|
36
|
+
def fourth( arg1="default" )
|
37
|
+
return "This is the fourth method (#{arg1})"
|
38
|
+
end
|
39
|
+
|
40
|
+
def fifth( *args )
|
41
|
+
return "This is the fifth method (#{args.collect do |arg| arg.inspect end.join(',')})"
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
### The test case
|
48
|
+
class TUMockTestCase < Test::Unit::TestCase
|
49
|
+
|
50
|
+
# Testing and setup methods that are exected to be available from MockObject
|
51
|
+
# instances. Does not include double-underscored and non-camelCased aliases,
|
52
|
+
# which are auto-generated by the test.
|
53
|
+
Interface = [
|
54
|
+
# Setup methods
|
55
|
+
:setReturnValues,
|
56
|
+
:setCallOrder,
|
57
|
+
:strictCallOrder=,
|
58
|
+
:strictCallOrder?,
|
59
|
+
|
60
|
+
# Testing methods
|
61
|
+
:callTrace,
|
62
|
+
:activate,
|
63
|
+
:verify,
|
64
|
+
:activate,
|
65
|
+
:clear,
|
66
|
+
:reset,
|
67
|
+
]
|
68
|
+
|
69
|
+
#################################################################
|
70
|
+
### C L A S S M E T H O D S
|
71
|
+
#################################################################
|
72
|
+
|
73
|
+
@@methodCounter = 0
|
74
|
+
@setupMethods = []
|
75
|
+
@teardownMethods = []
|
76
|
+
|
77
|
+
class << self
|
78
|
+
attr_accessor :setupMethods
|
79
|
+
attr_accessor :teardownMethods
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
### Append a setup block for the current testcase
|
84
|
+
def self::addSetupBlock( &block )
|
85
|
+
@@methodCounter += 1
|
86
|
+
newMethodName = "setup_#{@@methodCounter}".intern
|
87
|
+
define_method( newMethodName, &block )
|
88
|
+
self.setupMethods.push newMethodName
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
### Prepend a teardown block for the current testcase
|
93
|
+
def self::addTeardownBlock( &block )
|
94
|
+
@@methodCounter += 1
|
95
|
+
newMethodName = "teardown_#{@@methodCounter}".intern
|
96
|
+
define_method( newMethodName, &block )
|
97
|
+
self.teardownMethods.unshift newMethodName
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
#################################################################
|
102
|
+
### I N S T A N C E M E T H O D S
|
103
|
+
#################################################################
|
104
|
+
|
105
|
+
### Set up a test before running
|
106
|
+
def setup
|
107
|
+
self.class.setupMethods.each {|sblock|
|
108
|
+
self.send( sblock )
|
109
|
+
}
|
110
|
+
end
|
111
|
+
alias_method :set_up, :setup
|
112
|
+
|
113
|
+
### Tear down a test after it runs
|
114
|
+
def teardown
|
115
|
+
self.class.teardownMethods.each {|tblock|
|
116
|
+
self.send( tblock )
|
117
|
+
}
|
118
|
+
end
|
119
|
+
alias_method :tear_down, :teardown
|
120
|
+
|
121
|
+
|
122
|
+
#################################################################
|
123
|
+
### T E S T M E T H O D S
|
124
|
+
#################################################################
|
125
|
+
|
126
|
+
### Test to be sure the mockup class and factory method are loaded.
|
127
|
+
def test_00_loaded
|
128
|
+
assert_instance_of Class, Test::Unit::Mockup
|
129
|
+
assert_respond_to Test::Unit, :MockObject
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
### Test mock object creation/interface
|
134
|
+
def test_10_Constructor
|
135
|
+
mockClass = nil
|
136
|
+
mo = nil
|
137
|
+
|
138
|
+
assert_nothing_raised { mockClass = Test::Unit::MockObject(TestClass) }
|
139
|
+
assert_instance_of Class, mockClass
|
140
|
+
|
141
|
+
assert_nothing_raised { mo = mockClass::new }
|
142
|
+
assert_instance_of TestClass, mo
|
143
|
+
assert_kind_of TestClass, mo
|
144
|
+
assert mo.is_a?( TestClass ),
|
145
|
+
"Mock object doesn't fake the is_a? method"
|
146
|
+
assert mo.__class <= Test::Unit::Mockup,
|
147
|
+
"Mock object real type not a Test::Unit::Mockup derivative"
|
148
|
+
|
149
|
+
Interface.each {|meth|
|
150
|
+
|
151
|
+
# Test the regular method and the double-underscore one.
|
152
|
+
assert_respond_to mo, meth
|
153
|
+
assert_respond_to mo, "__#{meth.to_s}".intern
|
154
|
+
|
155
|
+
# Test for non-camelCased alias
|
156
|
+
if /[A-Z]/ =~ meth.to_s
|
157
|
+
# Oh, the irony.
|
158
|
+
nonCamelCaseMethod = meth.to_s.gsub( /([A-Z])/ ) {|match|
|
159
|
+
"_" + match.downcase
|
160
|
+
}.intern
|
161
|
+
|
162
|
+
assert_respond_to mo, nonCamelCaseMethod
|
163
|
+
end
|
164
|
+
|
165
|
+
}
|
166
|
+
|
167
|
+
self.class.addSetupBlock {
|
168
|
+
@mockObject = Test::Unit::MockObject(TestClass).new
|
169
|
+
}
|
170
|
+
self.class.addTeardownBlock {
|
171
|
+
@mockObject = nil
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
|
176
|
+
### Test the mocked methods
|
177
|
+
def test_15_FakedMethods
|
178
|
+
fakedMethod = nil
|
179
|
+
rval = nil
|
180
|
+
|
181
|
+
TestClass.instance_methods(false).each {|methName|
|
182
|
+
realMethod = TestClass.instance_method( methName )
|
183
|
+
|
184
|
+
assert_nothing_raised { fakedMethod = @mockObject.method(methName) }
|
185
|
+
assert_instance_of Method, fakedMethod
|
186
|
+
assert_equal realMethod.arity, fakedMethod.arity
|
187
|
+
}
|
188
|
+
|
189
|
+
@mockObject.activate
|
190
|
+
|
191
|
+
# Now test calling with and without correct arity
|
192
|
+
assert_raises( ArgumentError ) { @mockObject.first("foo") }
|
193
|
+
assert_nothing_raised { @mockObject.first }
|
194
|
+
|
195
|
+
assert_raises( ArgumentError ) { @mockObject.second }
|
196
|
+
assert_nothing_raised { @mockObject.second("foo") }
|
197
|
+
|
198
|
+
assert_raises( ArgumentError ) { @mockObject.third }
|
199
|
+
assert_nothing_raised { @mockObject.third("foo") }
|
200
|
+
|
201
|
+
assert_nothing_raised { @mockObject.fourth("foo") }
|
202
|
+
assert_nothing_raised { @mockObject.fourth }
|
203
|
+
|
204
|
+
assert_nothing_raised { @mockObject.fifth }
|
205
|
+
assert_nothing_raised { @mockObject.fifth("foo") }
|
206
|
+
assert_nothing_raised { @mockObject.fifth("foo", "bar", 1, 11, $stderr, 1..18) }
|
207
|
+
|
208
|
+
# Test for call trace
|
209
|
+
assert_nothing_raised { rval = @mockObject.callTrace }
|
210
|
+
assert_instance_of Array, rval
|
211
|
+
assert_equal 8, rval.length
|
212
|
+
|
213
|
+
debugMsg "Call trace: "
|
214
|
+
rval.each {|trace| debugMsg trace}
|
215
|
+
|
216
|
+
end
|
217
|
+
|
218
|
+
|
219
|
+
### Test the methods that aren't mocked (those that aren't already tested
|
220
|
+
### above)
|
221
|
+
def test_17_UnmockedMethods
|
222
|
+
rval = nil
|
223
|
+
|
224
|
+
# inspect
|
225
|
+
assert_nothing_raised { rval = @mockObject.inspect }
|
226
|
+
assert_match( /#<Class:0x[a-f0-9]+>:0x[a-f0-9]+ /, rval )
|
227
|
+
|
228
|
+
# class
|
229
|
+
assert_nothing_raised { rval = @mockObject.class }
|
230
|
+
assert_equal TestClass, rval
|
231
|
+
|
232
|
+
# method, send, respond_to?
|
233
|
+
assert_nothing_raised { rval = @mockObject.send(:inspect) }
|
234
|
+
assert_not_nil rval
|
235
|
+
assert_nothing_raised { rval = @mockObject.method(:inspect) }
|
236
|
+
assert_instance_of Method, rval
|
237
|
+
assert_nothing_raised { rval = @mockObject.respond_to?(:fifth) }
|
238
|
+
assert_equal true, rval
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
### Test the mock setup methods
|
243
|
+
def test_20_SetupMethods
|
244
|
+
assert_nothing_raised {
|
245
|
+
@mockObject.setReturnValues( :first => 1,
|
246
|
+
:second => 2,
|
247
|
+
:third => 3 )
|
248
|
+
}
|
249
|
+
assert_nothing_raised {
|
250
|
+
@mockObject.setReturnValues( :fourth => 4,
|
251
|
+
:second => 'II' )
|
252
|
+
}
|
253
|
+
|
254
|
+
assert_equal 1, @mockObject.first
|
255
|
+
assert_equal 'II', @mockObject.second(:foo)
|
256
|
+
assert_equal 3, @mockObject.third(:foo)
|
257
|
+
assert_equal 4, @mockObject.fourth
|
258
|
+
assert_equal true, @mockObject.fifth
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
### Test the call-order verification with "strict" mode turned off
|
263
|
+
def test_30_TestCallOrder
|
264
|
+
assert_nothing_raised {
|
265
|
+
@mockObject.setCallOrder( :first, :second, :third, :fourth, :fifth )
|
266
|
+
}
|
267
|
+
|
268
|
+
# Test a successful run with an extra call
|
269
|
+
assert_raises( RuntimeError ) { @mockObject.verify }
|
270
|
+
assert_nothing_raised { @mockObject.activate }
|
271
|
+
|
272
|
+
@mockObject.first
|
273
|
+
@mockObject.second(:foo)
|
274
|
+
@mockObject.arg1
|
275
|
+
@mockObject.third(:foo)
|
276
|
+
@mockObject.fourth
|
277
|
+
@mockObject.fifth
|
278
|
+
|
279
|
+
assert_raises( RuntimeError ) { @mockObject.activate }
|
280
|
+
assert_nothing_raised { @mockObject.verify }
|
281
|
+
|
282
|
+
# Test a failure
|
283
|
+
@mockObject.clear
|
284
|
+
@mockObject.activate
|
285
|
+
|
286
|
+
@mockObject.first
|
287
|
+
@mockObject.second(:foo)
|
288
|
+
@mockObject.arg1
|
289
|
+
@mockObject.fourth
|
290
|
+
@mockObject.fifth
|
291
|
+
|
292
|
+
assert_raises( Test::Unit::AssertionFailedError ) { @mockObject.verify }
|
293
|
+
end
|
294
|
+
|
295
|
+
|
296
|
+
### Test call-order verification with "strict" mode turned on
|
297
|
+
def test_35_TestStrictCallOrder
|
298
|
+
@mockObject.setCallOrder( :first, :second, :third, :fourth, :fifth )
|
299
|
+
assert_nothing_raised { @mockObject.strictCallOrder = true }
|
300
|
+
|
301
|
+
# Test a failure: an extra method
|
302
|
+
@mockObject.activate
|
303
|
+
|
304
|
+
@mockObject.first
|
305
|
+
@mockObject.second(:foo)
|
306
|
+
@mockObject.arg1
|
307
|
+
@mockObject.third(:foo)
|
308
|
+
@mockObject.fourth
|
309
|
+
@mockObject.fifth
|
310
|
+
|
311
|
+
assert_raises( Test::Unit::AssertionFailedError ) { @mockObject.verify }
|
312
|
+
|
313
|
+
# Test a failure for a missing method
|
314
|
+
@mockObject.clear
|
315
|
+
@mockObject.activate
|
316
|
+
|
317
|
+
@mockObject.first
|
318
|
+
@mockObject.second(:foo)
|
319
|
+
@mockObject.arg1
|
320
|
+
@mockObject.fourth
|
321
|
+
@mockObject.fifth
|
322
|
+
|
323
|
+
assert_raises( Test::Unit::AssertionFailedError ) { @mockObject.verify }
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
327
|
+
|
data/utils.rb
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
#
|
2
|
+
# Install/distribution utility functions
|
3
|
+
# $Id: utils.rb,v 1.2 2003/03/04 23:35:14 deveiant Exp $
|
4
|
+
#
|
5
|
+
# Copyright (c) 2001-2003, The FaerieMUD Consortium.
|
6
|
+
#
|
7
|
+
# This is free software. You may use, modify, and/or redistribute this
|
8
|
+
# software under the terms of the Perl Artistic License. (See
|
9
|
+
# http://language.perl.com/misc/Artistic.html)
|
10
|
+
#
|
11
|
+
|
12
|
+
|
13
|
+
BEGIN {
|
14
|
+
begin
|
15
|
+
require 'readline'
|
16
|
+
include Readline
|
17
|
+
rescue LoadError => e
|
18
|
+
$stderr.puts "Faking readline..."
|
19
|
+
def readline( prompt )
|
20
|
+
$stderr.print prompt.chomp
|
21
|
+
return $stdin.gets.chomp
|
22
|
+
end
|
23
|
+
end
|
24
|
+
}
|
25
|
+
|
26
|
+
module UtilityFunctions
|
27
|
+
|
28
|
+
# The list of regexen that eliminate files from the MANIFEST
|
29
|
+
ANTIMANIFEST = [
|
30
|
+
/makedist\.rb/,
|
31
|
+
/\bCVS\b/,
|
32
|
+
/~$/,
|
33
|
+
/^#/,
|
34
|
+
%r{docs/html},
|
35
|
+
%r{docs/man},
|
36
|
+
/^TEMPLATE/,
|
37
|
+
/\.cvsignore/,
|
38
|
+
/\.s?o$/
|
39
|
+
]
|
40
|
+
|
41
|
+
# Set some ANSI escape code constants (Shamelessly stolen from Perl's
|
42
|
+
# Term::ANSIColor by Russ Allbery <rra@stanford.edu> and Zenin <zenin@best.com>
|
43
|
+
AnsiAttributes = {
|
44
|
+
'clear' => 0,
|
45
|
+
'reset' => 0,
|
46
|
+
'bold' => 1,
|
47
|
+
'dark' => 2,
|
48
|
+
'underline' => 4,
|
49
|
+
'underscore' => 4,
|
50
|
+
'blink' => 5,
|
51
|
+
'reverse' => 7,
|
52
|
+
'concealed' => 8,
|
53
|
+
|
54
|
+
'black' => 30, 'on_black' => 40,
|
55
|
+
'red' => 31, 'on_red' => 41,
|
56
|
+
'green' => 32, 'on_green' => 42,
|
57
|
+
'yellow' => 33, 'on_yellow' => 43,
|
58
|
+
'blue' => 34, 'on_blue' => 44,
|
59
|
+
'magenta' => 35, 'on_magenta' => 45,
|
60
|
+
'cyan' => 36, 'on_cyan' => 46,
|
61
|
+
'white' => 37, 'on_white' => 47
|
62
|
+
}
|
63
|
+
|
64
|
+
ErasePreviousLine = "\033[A\033[K"
|
65
|
+
|
66
|
+
|
67
|
+
###############
|
68
|
+
module_function
|
69
|
+
###############
|
70
|
+
|
71
|
+
# Create a string that contains the ANSI codes specified and return it
|
72
|
+
def ansiCode( *attributes )
|
73
|
+
attr = attributes.collect {|a| AnsiAttributes[a] ? AnsiAttributes[a] : nil}.compact.join(';')
|
74
|
+
if attr.empty?
|
75
|
+
return ''
|
76
|
+
else
|
77
|
+
return "\e[%sm" % attr
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Test for the presence of the specified <tt>library</tt>, and output a
|
82
|
+
# message describing the test using <tt>nicename</tt>. If <tt>nicename</tt>
|
83
|
+
# is <tt>nil</tt>, the value in <tt>library</tt> is used to build a default.
|
84
|
+
def testForLibrary( library, nicename=nil )
|
85
|
+
nicename ||= library
|
86
|
+
message( "Testing for the #{nicename} library..." )
|
87
|
+
if $:.detect {|dir| File.exists?(File.join(dir,"#{library}.rb")) || File.exists?(File.join(dir,"#{library}.so"))}
|
88
|
+
message( "found.\n" )
|
89
|
+
return true
|
90
|
+
else
|
91
|
+
message( "not found.\n" )
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Test for the presence of the specified <tt>library</tt>, and output a
|
97
|
+
# message describing the problem using <tt>nicename</tt>. If
|
98
|
+
# <tt>nicename</tt> is <tt>nil</tt>, the value in <tt>library</tt> is used
|
99
|
+
# to build a default. If <tt>raaUrl</tt> and/or <tt>downloadUrl</tt> are
|
100
|
+
# specified, they are also use to build a message describing how to find the
|
101
|
+
# required library. If <tt>fatal</tt> is <tt>true</tt>, a missing library
|
102
|
+
# will cause the program to abort.
|
103
|
+
def testForRequiredLibrary( library, nicename=nil, raaUrl=nil, downloadUrl=nil, fatal=true )
|
104
|
+
nicename ||= library
|
105
|
+
unless testForLibrary( library, nicename )
|
106
|
+
msgs = [ "You are missing the required #{nicename} library.\n" ]
|
107
|
+
msgs << "RAA: #{raaUrl}\n" if raaUrl
|
108
|
+
msgs << "Download: #{downloadUrl}\n" if downloadUrl
|
109
|
+
if fatal
|
110
|
+
abort msgs.join('')
|
111
|
+
else
|
112
|
+
errorMessage msgs.join('')
|
113
|
+
end
|
114
|
+
end
|
115
|
+
return true
|
116
|
+
end
|
117
|
+
|
118
|
+
### Output <tt>msg</tt> as a ANSI-colored program/section header (white on
|
119
|
+
### blue).
|
120
|
+
def header( msg )
|
121
|
+
msg.chomp!
|
122
|
+
$stderr.puts ansiCode( 'bold', 'white', 'on_blue' ) + msg + ansiCode( 'reset' )
|
123
|
+
$stderr.flush
|
124
|
+
end
|
125
|
+
|
126
|
+
### Output <tt>msg</tt> to STDERR and flush it.
|
127
|
+
def message( msg )
|
128
|
+
$stderr.print msg
|
129
|
+
$stderr.flush
|
130
|
+
end
|
131
|
+
|
132
|
+
### Output the specified <tt>msg</tt> as an ANSI-colored error message
|
133
|
+
### (white on red).
|
134
|
+
def errorMessage( msg )
|
135
|
+
message ansiCode( 'bold', 'white', 'on_red' ) + msg + ansiCode( 'reset' )
|
136
|
+
end
|
137
|
+
|
138
|
+
### Output the specified <tt>msg</tt> as an ANSI-colored debugging message
|
139
|
+
### (yellow on blue).
|
140
|
+
def debugMsg( msg )
|
141
|
+
return unless $DEBUG
|
142
|
+
msg.chomp!
|
143
|
+
$stderr.puts ansiCode( 'bold', 'yellow', 'on_blue' ) + ">>> #{msg}" + ansiCode( 'reset' )
|
144
|
+
$stderr.flush
|
145
|
+
end
|
146
|
+
|
147
|
+
### Erase the previous line (if supported by your terminal) and output the
|
148
|
+
### specified <tt>msg</tt> instead.
|
149
|
+
def replaceMessage( msg )
|
150
|
+
print ErasePreviousLine
|
151
|
+
message( msg )
|
152
|
+
end
|
153
|
+
|
154
|
+
### Output a divider made up of <tt>length</tt> hyphen characters.
|
155
|
+
def divider( length=75 )
|
156
|
+
puts "\r" + ("-" * length )
|
157
|
+
end
|
158
|
+
alias :writeLine :divider
|
159
|
+
|
160
|
+
### Output the specified <tt>msg</tt> colored in ANSI red and exit with a
|
161
|
+
### status of 1.
|
162
|
+
def abort( msg )
|
163
|
+
print ansiCode( 'bold', 'red' ) + "Aborted: " + msg.chomp + ansiCode( 'reset' ) + "\n\n"
|
164
|
+
Kernel.exit!( 1 )
|
165
|
+
end
|
166
|
+
|
167
|
+
### Output the specified <tt>promptString</tt> as a prompt (in green) and
|
168
|
+
### return the user's input with leading and trailing spaces removed.
|
169
|
+
def prompt( promptString )
|
170
|
+
promptString.chomp!
|
171
|
+
return readline( ansiCode('bold', 'green') + "#{promptString}: " + ansiCode('reset') ).strip
|
172
|
+
end
|
173
|
+
|
174
|
+
### Prompt the user with the given <tt>promptString</tt> via #prompt,
|
175
|
+
### substituting the given <tt>default</tt> if the user doesn't input
|
176
|
+
### anything.
|
177
|
+
def promptWithDefault( promptString, default )
|
178
|
+
response = prompt( "%s [%s]" % [ promptString, default ] )
|
179
|
+
if response.empty?
|
180
|
+
return default
|
181
|
+
else
|
182
|
+
return response
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
### Search for the program specified by the given <tt>progname</tt> in the
|
187
|
+
### user's <tt>PATH</tt>, and return the full path to it, or <tt>nil</tt> if
|
188
|
+
### no such program is in the path.
|
189
|
+
def findProgram( progname )
|
190
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each {|d|
|
191
|
+
file = File.join( d, progname )
|
192
|
+
return file if File.executable?( file )
|
193
|
+
}
|
194
|
+
return nil
|
195
|
+
end
|
196
|
+
|
197
|
+
### Using the CVS log for the given <tt>file</tt> attempt to guess what the
|
198
|
+
### next release version might be. This only works if releases are tagged
|
199
|
+
### with tags like 'RELEASE_x_y'.
|
200
|
+
def extractNextVersionFromTags( file )
|
201
|
+
message "Attempting to extract next release version from CVS tags for #{file}...\n"
|
202
|
+
raise RuntimeError, "No such file '#{file}'" unless File.exists?( file )
|
203
|
+
cvsPath = findProgram( 'cvs' ) or
|
204
|
+
raise RuntimeError, "Cannot find the 'cvs' program. Aborting."
|
205
|
+
|
206
|
+
output = %x{#{cvsPath} log #{file}}
|
207
|
+
release = [ 0, 0 ]
|
208
|
+
output.scan( /RELEASE_(\d+)_(\d+)/ ) {|match|
|
209
|
+
if $1.to_i > release[0] || $2.to_i > release[1]
|
210
|
+
release = [ $1.to_i, $2.to_i ]
|
211
|
+
replaceMessage( "Found %d.%02d...\n" % release )
|
212
|
+
end
|
213
|
+
}
|
214
|
+
|
215
|
+
if release[1] >= 99
|
216
|
+
release[0] += 1
|
217
|
+
release[1] = 1
|
218
|
+
else
|
219
|
+
release[1] += 1
|
220
|
+
end
|
221
|
+
|
222
|
+
return "%d.%02d" % release
|
223
|
+
end
|
224
|
+
|
225
|
+
### Extract the project name (CVS Repository name) for the given directory.
|
226
|
+
def extractProjectName
|
227
|
+
File.open( "CVS/Repository", "r").readline.chomp
|
228
|
+
end
|
229
|
+
|
230
|
+
### Read the specified <tt>manifestFile</tt>, which is a text file
|
231
|
+
### describing which files to package up for a distribution. The manifest
|
232
|
+
### should consist of one or more lines, each containing one filename or
|
233
|
+
### shell glob pattern.
|
234
|
+
def readManifest( manifestFile="MANIFEST" )
|
235
|
+
message "Building manifest..."
|
236
|
+
raise "Missing #{manifestFile}, please remake it" unless File.exists? manifestFile
|
237
|
+
|
238
|
+
manifest = IO::readlines( manifestFile ).collect {|line|
|
239
|
+
line.chomp
|
240
|
+
}.select {|line|
|
241
|
+
line !~ /^(\s*(#.*)?)?$/
|
242
|
+
}
|
243
|
+
|
244
|
+
filelist = []
|
245
|
+
for pat in manifest
|
246
|
+
$stderr.puts "Adding files that match '#{pat}' to the file list" if $VERBOSE
|
247
|
+
filelist |= Dir.glob( pat ).find_all {|f| FileTest.file?(f)}
|
248
|
+
end
|
249
|
+
|
250
|
+
message "found #{filelist.length} files.\n"
|
251
|
+
return filelist
|
252
|
+
end
|
253
|
+
|
254
|
+
### Given a <tt>filelist</tt> like that returned by #readManifest, remove
|
255
|
+
### the entries therein which match the Regexp objects in the given
|
256
|
+
### <tt>antimanifest</tt> and return the resultant Array.
|
257
|
+
def vetManifest( filelist, antimanifest=ANITMANIFEST )
|
258
|
+
origLength = filelist.length
|
259
|
+
message "Vetting manifest..."
|
260
|
+
|
261
|
+
for regex in antimanifest
|
262
|
+
if $VERBOSE
|
263
|
+
message "\n\tPattern /#{regex.source}/ removed: " +
|
264
|
+
filelist.find_all {|file| regex.match(file)}.join(', ')
|
265
|
+
end
|
266
|
+
filelist.delete_if {|file| regex.match(file)}
|
267
|
+
end
|
268
|
+
|
269
|
+
message "removed #{origLength - filelist.length} files from the list.\n"
|
270
|
+
return filelist
|
271
|
+
end
|
272
|
+
|
273
|
+
### Combine a call to #readManifest with one to #vetManifest.
|
274
|
+
def getVettedManifest( manifestFile="MANIFEST", antimanifest=ANTIMANIFEST )
|
275
|
+
vetManifest( readManifest(manifestFile), antimanifest )
|
276
|
+
end
|
277
|
+
|
278
|
+
### Given a documentation <tt>catalogFile</tt>, which is in the same format
|
279
|
+
### as that described by #readManifest, read and expand it, and then return
|
280
|
+
### a list of those files which appear to have RDoc documentation in
|
281
|
+
### them. If <tt>catalogFile</tt> is nil or does not exist, the MANIFEST
|
282
|
+
### file is used instead.
|
283
|
+
def findRdocableFiles( catalogFile="docs/CATALOG" )
|
284
|
+
startlist = []
|
285
|
+
if File.exists? catalogFile
|
286
|
+
message "Using CATALOG file (%s).\n" % catalogFile
|
287
|
+
startlist = getVettedManifest( catalogFile )
|
288
|
+
else
|
289
|
+
message "Using default MANIFEST\n"
|
290
|
+
startlist = getVettedManifest()
|
291
|
+
end
|
292
|
+
|
293
|
+
message "Looking for RDoc comments in:\n" if $VERBOSE
|
294
|
+
startlist.select {|fn|
|
295
|
+
message " #{fn}: " if $VERBOSE
|
296
|
+
found = false
|
297
|
+
File::open( fn, "r" ) {|fh|
|
298
|
+
fh.each {|line|
|
299
|
+
if line =~ /^(\s*#)?\s*=/ || line =~ /:\w+:/ || line =~ %r{/\*}
|
300
|
+
found = true
|
301
|
+
break
|
302
|
+
end
|
303
|
+
}
|
304
|
+
}
|
305
|
+
|
306
|
+
message( (found ? "yes" : "no") + "\n" ) if $VERBOSE
|
307
|
+
found
|
308
|
+
}
|
309
|
+
end
|
310
|
+
|
311
|
+
### Open a file and filter each of its lines through the given block a
|
312
|
+
### <tt>line</tt> at a time. The return value of the block is used as the
|
313
|
+
### new line, or omitted if the block returns <tt>nil</tt> or
|
314
|
+
### <tt>false</tt>.
|
315
|
+
def editInPlace( file ) # :yields: line
|
316
|
+
raise "No block specified for editing operation" unless block_given?
|
317
|
+
|
318
|
+
tempName = "#{file}.#{$$}"
|
319
|
+
File::open( tempName, File::RDWR|File::CREAT, 0600 ) {|tempfile|
|
320
|
+
File::unlink( tempName )
|
321
|
+
File::open( file, File::RDONLY ) {|fh|
|
322
|
+
fh.each {|line|
|
323
|
+
newline = yield( line ) or next
|
324
|
+
tempfile.print( newline )
|
325
|
+
}
|
326
|
+
}
|
327
|
+
|
328
|
+
tempfile.seek(0)
|
329
|
+
|
330
|
+
File::open( file, File::TRUNC|File::WRONLY, 0644 ) {|newfile|
|
331
|
+
newfile.print( tempfile.read )
|
332
|
+
}
|
333
|
+
}
|
334
|
+
end
|
335
|
+
|
336
|
+
### Execute the specified shell <tt>command</tt>, read the results, and
|
337
|
+
### return them. Like a %x{} that returns an Array instead of a String.
|
338
|
+
def shellCommand( *command )
|
339
|
+
raise "Empty command" if command.empty?
|
340
|
+
|
341
|
+
cmdpipe = IO::popen( command.join(' '), 'r' )
|
342
|
+
return cmdpipe.readlines
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|