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.
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
+ #