sitefuel 0.0.0a

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/README +86 -0
  2. data/RELEASE_NOTES +7 -0
  3. data/bin/sitefuel +162 -0
  4. data/lib/sitefuel/Configuration.rb +35 -0
  5. data/lib/sitefuel/SiteFuelLogger.rb +128 -0
  6. data/lib/sitefuel/SiteFuelRuntime.rb +293 -0
  7. data/lib/sitefuel/extensions/ArrayComparisons.rb +34 -0
  8. data/lib/sitefuel/extensions/DynamicClassMethods.rb +19 -0
  9. data/lib/sitefuel/extensions/FileComparison.rb +24 -0
  10. data/lib/sitefuel/extensions/Silently.rb +27 -0
  11. data/lib/sitefuel/extensions/StringFormatting.rb +75 -0
  12. data/lib/sitefuel/extensions/SymbolComparison.rb +13 -0
  13. data/lib/sitefuel/external/AbstractExternalProgram.rb +616 -0
  14. data/lib/sitefuel/external/ExternalProgramTestCase.rb +67 -0
  15. data/lib/sitefuel/external/GIT.rb +9 -0
  16. data/lib/sitefuel/external/JPEGTran.rb +68 -0
  17. data/lib/sitefuel/external/PNGCrush.rb +66 -0
  18. data/lib/sitefuel/processors/AbstractExternalProgramProcessor.rb +72 -0
  19. data/lib/sitefuel/processors/AbstractProcessor.rb +378 -0
  20. data/lib/sitefuel/processors/AbstractStringBasedProcessor.rb +88 -0
  21. data/lib/sitefuel/processors/CSSProcessor.rb +84 -0
  22. data/lib/sitefuel/processors/HAMLProcessor.rb +52 -0
  23. data/lib/sitefuel/processors/HTMLProcessor.rb +211 -0
  24. data/lib/sitefuel/processors/JavaScriptProcessor.rb +57 -0
  25. data/lib/sitefuel/processors/PHPProcessor.rb +32 -0
  26. data/lib/sitefuel/processors/PNGProcessor.rb +80 -0
  27. data/lib/sitefuel/processors/RHTMLProcessor.rb +25 -0
  28. data/lib/sitefuel/processors/SASSProcessor.rb +50 -0
  29. data/test/processor_listing.rb +28 -0
  30. data/test/test_AbstractExternalProgram.rb +186 -0
  31. data/test/test_AbstractProcessor.rb +237 -0
  32. data/test/test_AbstractStringBasedProcessor.rb +48 -0
  33. data/test/test_AllProcessors.rb +65 -0
  34. data/test/test_ArrayComparisons.rb +32 -0
  35. data/test/test_CSSProcessor.rb +120 -0
  36. data/test/test_FileComparisons.rb +42 -0
  37. data/test/test_HAMLProcessor.rb.rb +60 -0
  38. data/test/test_HTMLProcessor.rb +186 -0
  39. data/test/test_JPEGTran.rb +40 -0
  40. data/test/test_JavaScriptProcessor.rb +63 -0
  41. data/test/test_PHPProcessor.rb +51 -0
  42. data/test/test_PNGCrush.rb +58 -0
  43. data/test/test_PNGProcessor.rb +32 -0
  44. data/test/test_RHTMLProcessor.rb +62 -0
  45. data/test/test_SASSProcessor.rb +68 -0
  46. data/test/test_SiteFuelLogging.rb +79 -0
  47. data/test/test_SiteFuelRuntime.rb +96 -0
  48. data/test/test_StringFormatting.rb +51 -0
  49. data/test/test_SymbolComparison.rb +27 -0
  50. data/test/test_images/sample_jpg01.jpg +0 -0
  51. data/test/test_images/sample_png01.png +0 -0
  52. data/test/test_programs/versioning.rb +26 -0
  53. data/test/test_sites/simplehtml/deployment.yml +22 -0
  54. data/test/test_sites/simplehtml/index.html +66 -0
  55. data/test/test_sites/simplehtml/style.css +40 -0
  56. data/test/ts_all.rb +39 -0
  57. 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
+
@@ -0,0 +1,9 @@
1
+ #
2
+ # File:: GIT.rb
3
+ # Author:: wkm
4
+ # Copyright:: 2009
5
+ # License:: GPL
6
+ #
7
+ # Wrapper around the git version control system.
8
+ #
9
+ #