sitefuel 0.0.0a
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/README +86 -0
- data/RELEASE_NOTES +7 -0
- data/bin/sitefuel +162 -0
- data/lib/sitefuel/Configuration.rb +35 -0
- data/lib/sitefuel/SiteFuelLogger.rb +128 -0
- data/lib/sitefuel/SiteFuelRuntime.rb +293 -0
- data/lib/sitefuel/extensions/ArrayComparisons.rb +34 -0
- data/lib/sitefuel/extensions/DynamicClassMethods.rb +19 -0
- data/lib/sitefuel/extensions/FileComparison.rb +24 -0
- data/lib/sitefuel/extensions/Silently.rb +27 -0
- data/lib/sitefuel/extensions/StringFormatting.rb +75 -0
- data/lib/sitefuel/extensions/SymbolComparison.rb +13 -0
- data/lib/sitefuel/external/AbstractExternalProgram.rb +616 -0
- data/lib/sitefuel/external/ExternalProgramTestCase.rb +67 -0
- data/lib/sitefuel/external/GIT.rb +9 -0
- data/lib/sitefuel/external/JPEGTran.rb +68 -0
- data/lib/sitefuel/external/PNGCrush.rb +66 -0
- data/lib/sitefuel/processors/AbstractExternalProgramProcessor.rb +72 -0
- data/lib/sitefuel/processors/AbstractProcessor.rb +378 -0
- data/lib/sitefuel/processors/AbstractStringBasedProcessor.rb +88 -0
- data/lib/sitefuel/processors/CSSProcessor.rb +84 -0
- data/lib/sitefuel/processors/HAMLProcessor.rb +52 -0
- data/lib/sitefuel/processors/HTMLProcessor.rb +211 -0
- data/lib/sitefuel/processors/JavaScriptProcessor.rb +57 -0
- data/lib/sitefuel/processors/PHPProcessor.rb +32 -0
- data/lib/sitefuel/processors/PNGProcessor.rb +80 -0
- data/lib/sitefuel/processors/RHTMLProcessor.rb +25 -0
- data/lib/sitefuel/processors/SASSProcessor.rb +50 -0
- data/test/processor_listing.rb +28 -0
- data/test/test_AbstractExternalProgram.rb +186 -0
- data/test/test_AbstractProcessor.rb +237 -0
- data/test/test_AbstractStringBasedProcessor.rb +48 -0
- data/test/test_AllProcessors.rb +65 -0
- data/test/test_ArrayComparisons.rb +32 -0
- data/test/test_CSSProcessor.rb +120 -0
- data/test/test_FileComparisons.rb +42 -0
- data/test/test_HAMLProcessor.rb.rb +60 -0
- data/test/test_HTMLProcessor.rb +186 -0
- data/test/test_JPEGTran.rb +40 -0
- data/test/test_JavaScriptProcessor.rb +63 -0
- data/test/test_PHPProcessor.rb +51 -0
- data/test/test_PNGCrush.rb +58 -0
- data/test/test_PNGProcessor.rb +32 -0
- data/test/test_RHTMLProcessor.rb +62 -0
- data/test/test_SASSProcessor.rb +68 -0
- data/test/test_SiteFuelLogging.rb +79 -0
- data/test/test_SiteFuelRuntime.rb +96 -0
- data/test/test_StringFormatting.rb +51 -0
- data/test/test_SymbolComparison.rb +27 -0
- data/test/test_images/sample_jpg01.jpg +0 -0
- data/test/test_images/sample_png01.png +0 -0
- data/test/test_programs/versioning.rb +26 -0
- data/test/test_sites/simplehtml/deployment.yml +22 -0
- data/test/test_sites/simplehtml/index.html +66 -0
- data/test/test_sites/simplehtml/style.css +40 -0
- data/test/ts_all.rb +39 -0
- metadata +165 -0
@@ -0,0 +1,616 @@
|
|
1
|
+
#
|
2
|
+
# File:: AbstractExternalProgram.rb
|
3
|
+
# Author:: wkm
|
4
|
+
# Copyright:: 2009
|
5
|
+
# License:: GPL
|
6
|
+
#
|
7
|
+
# An abstraction around calling an external program.
|
8
|
+
#
|
9
|
+
# TODO: make this less dependent on the OS behaving like Linux/OS X.
|
10
|
+
# TODO: the general API here is still far from thought out.
|
11
|
+
#
|
12
|
+
|
13
|
+
module SiteFuel
|
14
|
+
module External
|
15
|
+
|
16
|
+
require 'sitefuel/extensions/DynamicClassMethods'
|
17
|
+
require 'sitefuel/SiteFuelLogger'
|
18
|
+
|
19
|
+
# raised when an external program can't be found
|
20
|
+
class ProgramNotFound < StandardError
|
21
|
+
attr_reader :program_name
|
22
|
+
def initialize(program_name)
|
23
|
+
@program_name = program_name
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
# raised when the program appears to be found but it's version is not
|
30
|
+
# compatible
|
31
|
+
class VersionNotFound < StandardError
|
32
|
+
attr_reader :program_name, :compatible_version, :actual_version
|
33
|
+
def initialize(program_name, compatible_version, actual_version)
|
34
|
+
@program_name = program_name
|
35
|
+
@compatible_version = compatible_version
|
36
|
+
@actual_version = actual_version
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_s
|
40
|
+
'Compatible versions of program %s are %s. Found version %s' %
|
41
|
+
[program_name, compatible_version, actual_version]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
# raised when an option is given that isn't known
|
48
|
+
class UnknownOption < StandardError
|
49
|
+
attr_reader :program, :option_name
|
50
|
+
def initialize(program, option_name)
|
51
|
+
@program = program
|
52
|
+
@option_name = option_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
'Program %s doesn\'t have option %s' %
|
57
|
+
[program, option_name]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
# because of the AbstractExternalProgram's API design there are certain
|
64
|
+
# option names that are disallowed (see
|
65
|
+
# AbstractExternalProgram#excluded_option_names)
|
66
|
+
class UnallowedOptionName < StandardError
|
67
|
+
attr_reader :program, :option_name, :excluded_names
|
68
|
+
def initialize(program, option_name, excluded_names)
|
69
|
+
@program = program
|
70
|
+
@option_name = option_name
|
71
|
+
@excluded_names = excluded_names
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
'Program %s declares option %s which has one of the illegal option names: %s' %
|
76
|
+
[program, option_name, excluded_names]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
|
82
|
+
# AbstractExternalProgram#execute and friends have a somewhat strange syntax
|
83
|
+
# for accepting options and flags. This exception is raised when the syntax
|
84
|
+
# is malformed.
|
85
|
+
class MalformedOptions < StandardError
|
86
|
+
attr_reader :program, :options
|
87
|
+
def initialize(program, options)
|
88
|
+
@program = program
|
89
|
+
@options = options
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_s
|
93
|
+
'Program %s called with a malformed options pattern: %s' %
|
94
|
+
[program, options.join(' ')]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
|
100
|
+
# raised when a default is specified for an option without a value slot in
|
101
|
+
# the option template
|
102
|
+
class NoOptionValueSlot < StandardError
|
103
|
+
attr_reader :program, :option_name
|
104
|
+
def initialize(program, option_name)
|
105
|
+
@program = program
|
106
|
+
@option_name = option_name
|
107
|
+
end
|
108
|
+
|
109
|
+
def to_s
|
110
|
+
'Program %s has default value but no option slot for option %s' %
|
111
|
+
[program, option_name]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
# raised when a option requires a value (because no default is specified)
|
118
|
+
# but none is given
|
119
|
+
class NoValueForOption < StandardError
|
120
|
+
attr_reader :program, :option_name
|
121
|
+
def initialize(program, option_name)
|
122
|
+
@program = program
|
123
|
+
@option_name = option_name
|
124
|
+
end
|
125
|
+
|
126
|
+
def to_s
|
127
|
+
'Program %s option %s requires a value, but no value was specified' %
|
128
|
+
[program, option_name]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
# lightweight abstraction around a program external to Ruby. The class
|
139
|
+
# is designed to make it easy to use an external program in a batch
|
140
|
+
# fashion. Note that the abstraction does not well support interacting
|
141
|
+
# back and forth with external programs.
|
142
|
+
class AbstractExternalProgram
|
143
|
+
|
144
|
+
include SiteFuel::Logging
|
145
|
+
|
146
|
+
VERSION_SEPARATOR = '.'
|
147
|
+
|
148
|
+
# cache of whether compatible versions of programs exist
|
149
|
+
@@compatible_versions = {}
|
150
|
+
|
151
|
+
# cache of whether the actual programs that are abstracted exist
|
152
|
+
@@program_exists = {}
|
153
|
+
|
154
|
+
#
|
155
|
+
@@program_binary = {}
|
156
|
+
@@program_options = {}
|
157
|
+
|
158
|
+
@@option_struct = Struct.new('ExternalProgramOption', 'name', 'template', 'default')
|
159
|
+
|
160
|
+
# classes which implement AbstractExternalProgram need to define
|
161
|
+
# a self.program_name method.
|
162
|
+
|
163
|
+
|
164
|
+
# gives the location of the external program; uses the =which=
|
165
|
+
# unix command
|
166
|
+
def self.program_binary
|
167
|
+
|
168
|
+
# give the cached version if possible
|
169
|
+
cached = @@program_binary[self]
|
170
|
+
return cached if cached
|
171
|
+
|
172
|
+
# otherwise try to find it:
|
173
|
+
binary = capture_output("which", program_name)
|
174
|
+
if binary.empty?
|
175
|
+
raise ProgramNotFound.new(program_name)
|
176
|
+
else
|
177
|
+
# ensure the binary is resolved with respect to the root path
|
178
|
+
binary = File.expand_path(binary, capture_output('pwd'))
|
179
|
+
@@program_binary[self] = binary
|
180
|
+
binary
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
# Similar to Kernel#exec, but returns a string of the output
|
186
|
+
def self.capture_output(command, *args)
|
187
|
+
cli = command + ' ' + args.join(' ')
|
188
|
+
|
189
|
+
|
190
|
+
# if we want to capture stderr we need to redirect to stdout
|
191
|
+
if capture_stderr
|
192
|
+
cli << ' 2>&1'
|
193
|
+
end
|
194
|
+
|
195
|
+
output_string = ""
|
196
|
+
IO.popen(cli, 'r') do |io|
|
197
|
+
output_string = io.read.chop
|
198
|
+
end
|
199
|
+
output_string
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
# gives true if the program can be found.
|
204
|
+
def self.program_found?
|
205
|
+
program_binary
|
206
|
+
rescue ProgramNotFound
|
207
|
+
false
|
208
|
+
else
|
209
|
+
true
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
# gives a condition on the compatible versions. A version is considered compatible
|
214
|
+
# if it's greater than the given version. Eventually we'll probably need a way to
|
215
|
+
# give a version range and allow excluding particular versions.
|
216
|
+
def self.compatible_versions
|
217
|
+
'> 0.0.0'
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
# gives true if a binary with a compatible version exists
|
222
|
+
def self.compatible_version?
|
223
|
+
compatible_version_number?(program_version)
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
# gives true if the given version is newer.
|
228
|
+
# TODO this should be replaced by a proper version handling library (eg.
|
229
|
+
# versionometry (sp?))
|
230
|
+
def self.version_older? (lhs, rhs)
|
231
|
+
return true if lhs == rhs
|
232
|
+
|
233
|
+
# split into separate version chunks
|
234
|
+
lhs = lhs.split(VERSION_SEPARATOR)
|
235
|
+
rhs = rhs.split(VERSION_SEPARATOR)
|
236
|
+
|
237
|
+
# if lhs is shorter than the rhs must be greater than or equal to the
|
238
|
+
# lhs; but if the lhs is *longer* than the rhs must be greater than the
|
239
|
+
# lhs.
|
240
|
+
if lhs.length > rhs.length
|
241
|
+
lhs = lhs[0...rhs.length]
|
242
|
+
method = :<
|
243
|
+
else
|
244
|
+
method = :<=
|
245
|
+
rhs = rhs[0...lhs.length]
|
246
|
+
end
|
247
|
+
|
248
|
+
# now compare
|
249
|
+
lhs.join(VERSION_SEPARATOR).send(method, rhs.join(VERSION_SEPARATOR))
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
# tests a version number against a list of compatible version specifications
|
254
|
+
# should be made into a Version class. We could also expand the Gem::Version
|
255
|
+
# class and use that....
|
256
|
+
def self.test_version_number(compatible, version_number)
|
257
|
+
# ensure we're dealing with an array
|
258
|
+
version_scheme = compatible
|
259
|
+
if not version_scheme.kind_of? Array
|
260
|
+
version_scheme = [version_scheme]
|
261
|
+
end
|
262
|
+
|
263
|
+
version_scheme.each do |ver|
|
264
|
+
case ver[0..0]
|
265
|
+
when '>'
|
266
|
+
return version_older?(ver[1..-1].strip, version_number)
|
267
|
+
when '<'
|
268
|
+
return !version_older?(ver[1..-1].strip, version_number)
|
269
|
+
else
|
270
|
+
# ignore this version spec
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
return false
|
275
|
+
end
|
276
|
+
|
277
|
+
|
278
|
+
# gives true if a given version number is compatible
|
279
|
+
def self.compatible_version_number?(version_number)
|
280
|
+
self.test_version_number(compatible_versions, version_number)
|
281
|
+
end
|
282
|
+
|
283
|
+
|
284
|
+
# raises the ProgramNotFound error if the program can't be found
|
285
|
+
# See also AbstractExternalProgram.verify_compatible_version
|
286
|
+
def self.verify_program_exists
|
287
|
+
if @@program_exists[self] == nil
|
288
|
+
@@program_exists[self] = program_found?
|
289
|
+
end
|
290
|
+
|
291
|
+
if @@program_exists[self] == true
|
292
|
+
return true
|
293
|
+
else
|
294
|
+
raise ProgramNotFound(self)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
# raises the ProgramNotFound error if the program can't be found
|
300
|
+
# raises the VersionNotFound error if a compatible version isn't found.
|
301
|
+
# the verification is cached using a class variable so the verification
|
302
|
+
# only actually happens the first time.
|
303
|
+
#
|
304
|
+
# Because of the caching this function is generally very fast and should
|
305
|
+
# be called by every method that actually will execute the program.
|
306
|
+
def self.verify_compatible_version
|
307
|
+
verify_program_exists
|
308
|
+
|
309
|
+
if @@compatible_versions[self] == nil
|
310
|
+
@@compatible_versions[self] = compatible_version?
|
311
|
+
end
|
312
|
+
|
313
|
+
if @@compatible_versions[self] == true
|
314
|
+
return true
|
315
|
+
else
|
316
|
+
raise VersionNotFound.new(self, self.compatible_versions, self.program_version)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
# given the output of a program gives the version number or nil
|
322
|
+
# if not available
|
323
|
+
def self.extract_program_version(version_output)
|
324
|
+
version_output[/(\d+\.\d+(\.\d+)?([a-zA-Z]+)?)/]
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
# option for giving the version of the program
|
329
|
+
def self.option_version
|
330
|
+
'--version'
|
331
|
+
end
|
332
|
+
|
333
|
+
|
334
|
+
# gets the version of a program
|
335
|
+
def self.program_version
|
336
|
+
extract_program_version(capture_output(program_binary, option_version))
|
337
|
+
rescue ProgramNotFound
|
338
|
+
return nil
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
# calls an option
|
343
|
+
def self.call_option(option_name)
|
344
|
+
method_name = "option_"+option_name.to_s
|
345
|
+
self.send(method_name.to_sym)
|
346
|
+
end
|
347
|
+
|
348
|
+
|
349
|
+
# gives the listing of declared options for the program
|
350
|
+
def self.options
|
351
|
+
names = methods
|
352
|
+
names = names.delete_if { |method| not method =~ /^option_.*$/ }
|
353
|
+
names.sort!
|
354
|
+
|
355
|
+
names = names.map { |option_name| option_name.sub(/^option_(.*)$/, '\1').to_sym }
|
356
|
+
names - excluded_option_names
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
# controls what happens with the output from the program
|
361
|
+
# =capture=:: output is captured into a string and returned from #execute
|
362
|
+
# =forward=:: output is forwarded to the terminal as normal
|
363
|
+
def self.output_handling
|
364
|
+
:capture
|
365
|
+
end
|
366
|
+
|
367
|
+
|
368
|
+
# list of excluded option names
|
369
|
+
def self.excluded_option_names
|
370
|
+
[:default, :template]
|
371
|
+
end
|
372
|
+
|
373
|
+
|
374
|
+
# tests whether a given option name is allowed
|
375
|
+
def self.allowed_option_name?(name)
|
376
|
+
not excluded_option_names.include?(name.to_sym)
|
377
|
+
end
|
378
|
+
|
379
|
+
|
380
|
+
# gives the default value for an option
|
381
|
+
def self.option_default(option_name)
|
382
|
+
ensure_valid_option(option_name)
|
383
|
+
self.call_option(option_name).default
|
384
|
+
end
|
385
|
+
|
386
|
+
|
387
|
+
# gives the template for an option
|
388
|
+
def self.option_template(option_name)
|
389
|
+
ensure_valid_option(option_name)
|
390
|
+
self.call_option(option_name).template
|
391
|
+
end
|
392
|
+
|
393
|
+
|
394
|
+
# gives true if given a known option
|
395
|
+
def self.option?(name)
|
396
|
+
self.options.include?(name)
|
397
|
+
end
|
398
|
+
|
399
|
+
|
400
|
+
# raises UnknownOption error if the given option isn't valid
|
401
|
+
def self.ensure_valid_option(name)
|
402
|
+
if not option?(name)
|
403
|
+
raise UnknownOption.new(self, name)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
|
408
|
+
# declares an option for this program
|
409
|
+
def self.option(name, template = nil, default = nil)
|
410
|
+
unless name.kind_of? String
|
411
|
+
name = name.to_s
|
412
|
+
end
|
413
|
+
|
414
|
+
unless allowed_option_name?(name)
|
415
|
+
raise UnallowedOptionName.new(self, name, excluded_option_names)
|
416
|
+
end
|
417
|
+
|
418
|
+
# if a default is given but the template has no value slot...
|
419
|
+
if default != nil and not template.include?('${value}')
|
420
|
+
raise NoOptionValueSlot.new(self, name)
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
# give a method for the option
|
425
|
+
method_name = "option_"+name
|
426
|
+
struct = @@option_struct.new(name, template, default)
|
427
|
+
define_class_method(method_name.to_sym) { struct }
|
428
|
+
end
|
429
|
+
|
430
|
+
# organizes a list of options into a ragged array of arrays
|
431
|
+
#
|
432
|
+
# organize_options(:setflag, :paramsetting, 'val1', 'val2')
|
433
|
+
# # =>[[:setflag], [:paramsetting, 'val1', 'val2']]
|
434
|
+
def self.organize_options(*options)
|
435
|
+
organized = []
|
436
|
+
i = 0
|
437
|
+
|
438
|
+
while i < options.length
|
439
|
+
# if we see a symbol are at a new option
|
440
|
+
if options[i].kind_of? Symbol
|
441
|
+
option_row = [options[i]]
|
442
|
+
organized << option_row
|
443
|
+
|
444
|
+
j = i+1
|
445
|
+
while j < options.length
|
446
|
+
case options[j]
|
447
|
+
when String
|
448
|
+
# adds this value
|
449
|
+
option_row << options[j]
|
450
|
+
j += 1
|
451
|
+
|
452
|
+
when Symbol
|
453
|
+
# the zoom below will cause this spot to get looked at
|
454
|
+
break
|
455
|
+
|
456
|
+
else
|
457
|
+
# the zoom below will force us to look at this spot again
|
458
|
+
# and bail
|
459
|
+
break
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# zoom i ahead to this spot
|
464
|
+
i = j
|
465
|
+
else
|
466
|
+
raise MalformedOptions.new(self, options)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
return organized
|
471
|
+
end
|
472
|
+
|
473
|
+
|
474
|
+
|
475
|
+
# creates and executes an instance of this external program by taking
|
476
|
+
# a list of parameters and their values
|
477
|
+
#
|
478
|
+
# self.execute :setflag, # set a flag
|
479
|
+
# :paramsetting, "param value", # pass a single value
|
480
|
+
# :paramsetting2, "val1", "val2" # pass multiple values
|
481
|
+
def self.execute(*options)
|
482
|
+
instance = self.new
|
483
|
+
organized = organize_options(*options)
|
484
|
+
|
485
|
+
organized.each do |opt|
|
486
|
+
instance.add_option(opt)
|
487
|
+
end
|
488
|
+
|
489
|
+
instance.execute
|
490
|
+
end
|
491
|
+
|
492
|
+
|
493
|
+
|
494
|
+
|
495
|
+
|
496
|
+
#
|
497
|
+
# INSTANCE METHODS
|
498
|
+
#
|
499
|
+
|
500
|
+
attr_reader :options
|
501
|
+
|
502
|
+
def initialize
|
503
|
+
# check that a compatible version exists
|
504
|
+
self.class.verify_compatible_version
|
505
|
+
|
506
|
+
self.logger = SiteFuelLogger.instance
|
507
|
+
@options = []
|
508
|
+
end
|
509
|
+
|
510
|
+
|
511
|
+
# ensures the option specification makes sense
|
512
|
+
def ensure_option_validity(option_row)
|
513
|
+
name = option_row.first
|
514
|
+
if requires_value?(name) and option_row.length < 2
|
515
|
+
raise NoValueForOption.new(self.class, name)
|
516
|
+
end
|
517
|
+
|
518
|
+
true
|
519
|
+
end
|
520
|
+
|
521
|
+
|
522
|
+
# gives true if the given option is valid
|
523
|
+
def valid_option?(option_row)
|
524
|
+
ensure_option_validity(option_row)
|
525
|
+
return true
|
526
|
+
rescue
|
527
|
+
return false
|
528
|
+
end
|
529
|
+
|
530
|
+
|
531
|
+
# adds an option to be passed to this instance
|
532
|
+
def add_option(option_row)
|
533
|
+
ensure_option_validity(option_row)
|
534
|
+
|
535
|
+
case option_row
|
536
|
+
when Array
|
537
|
+
@options << option_row
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
|
542
|
+
# gives the template for an option
|
543
|
+
def option_template(name)
|
544
|
+
self.class.option_template(name)
|
545
|
+
end
|
546
|
+
|
547
|
+
|
548
|
+
# generally we don't want to capture stderr since it helps users
|
549
|
+
# with finding out why things don't work. In certain cases we do
|
550
|
+
# need to capture it, however.
|
551
|
+
def self.capture_stderr
|
552
|
+
false
|
553
|
+
end
|
554
|
+
|
555
|
+
|
556
|
+
# applies a given value into an option template
|
557
|
+
def apply_value(option_template, value)
|
558
|
+
option_template.gsub('${value}', value)
|
559
|
+
end
|
560
|
+
|
561
|
+
|
562
|
+
# returns true if a given option takes a value
|
563
|
+
# TODO this should be precomputed
|
564
|
+
def takes_value? (name)
|
565
|
+
option_template(name).include?('${value}')
|
566
|
+
end
|
567
|
+
|
568
|
+
|
569
|
+
# gives true if an option has a default
|
570
|
+
def has_default?(name)
|
571
|
+
self.class.option_default(name) != nil
|
572
|
+
end
|
573
|
+
|
574
|
+
|
575
|
+
# gives true if an option takes a value but has no default
|
576
|
+
def requires_value?(name)
|
577
|
+
takes_value?(name) and not has_default?(name)
|
578
|
+
end
|
579
|
+
|
580
|
+
|
581
|
+
# executes the given AbstractExternalProgram instance
|
582
|
+
def execute
|
583
|
+
self.class.verify_compatible_version
|
584
|
+
|
585
|
+
exec_string = self.class.program_binary.clone
|
586
|
+
@options.each do |option_row|
|
587
|
+
option_string = option_template(option_row.first)
|
588
|
+
case option_row.length
|
589
|
+
when 1
|
590
|
+
if takes_value?(option_row.first)
|
591
|
+
option_string = apply_value(option_string, self.class.option_default(option_row.first))
|
592
|
+
end
|
593
|
+
|
594
|
+
when 2
|
595
|
+
option_string = apply_value(option_string, option_row[1])
|
596
|
+
|
597
|
+
else
|
598
|
+
option_string = ''
|
599
|
+
end
|
600
|
+
exec_string << ' ' << option_string
|
601
|
+
end
|
602
|
+
|
603
|
+
info 'Executing: '+exec_string
|
604
|
+
|
605
|
+
case self.class.output_handling
|
606
|
+
when :capture
|
607
|
+
self.class.capture_output(exec_string)
|
608
|
+
|
609
|
+
when :forward
|
610
|
+
exec(exec_string)
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
end
|
615
|
+
end
|
616
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
#
|
2
|
+
# File:: ExternalProgramTestCase.rb
|
3
|
+
# Author:: wkm
|
4
|
+
# Copyright:: 2009
|
5
|
+
# License:: GPl
|
6
|
+
#
|
7
|
+
# Lightweight utility that will effectively scrap
|
8
|
+
# the entire test case if the program being tested doesn't exist.
|
9
|
+
#
|
10
|
+
|
11
|
+
require 'test/unit'
|
12
|
+
require 'term/ansicolor'
|
13
|
+
|
14
|
+
module SiteFuel
|
15
|
+
module External
|
16
|
+
|
17
|
+
include Term::ANSIColor
|
18
|
+
|
19
|
+
class Test::Unit::TestCase
|
20
|
+
# exposes the private #define_method function to the world
|
21
|
+
def self.publicly_define_method(name, &block)
|
22
|
+
define_method(name, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ExternalProgramTestCase
|
27
|
+
|
28
|
+
@@message_posted = {}
|
29
|
+
|
30
|
+
def get_tested_class
|
31
|
+
name = self.class.to_s.gsub(/^(.*?::)?Test(.*)$/, "\\2")
|
32
|
+
|
33
|
+
# ensure the class exists
|
34
|
+
cls = Kernel.const_get(name)
|
35
|
+
|
36
|
+
return cls if cls != nil
|
37
|
+
return nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def initialize(*args)
|
41
|
+
cls = get_tested_class
|
42
|
+
unless cls == nil
|
43
|
+
if cls.program_found?
|
44
|
+
# amusing pun.
|
45
|
+
super(*args)
|
46
|
+
else
|
47
|
+
if not @@message_posted[self.class]
|
48
|
+
puts "Ignoring #{cls} unit tests. Program #{cls.program_name} not found.".bold
|
49
|
+
@@message_posted[self.class] = true
|
50
|
+
end
|
51
|
+
|
52
|
+
# fun part. Over-ride every method beginning with test* so they are nops
|
53
|
+
methods = self.methods
|
54
|
+
methods.each do |name|
|
55
|
+
if name =~ /^test.*$/
|
56
|
+
self.class.publicly_define_method(name) {}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
super(*args)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|