patir 0.2

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.
@@ -0,0 +1,18 @@
1
+ # -*- ruby -*-
2
+ $:.unshift File.join(File.dirname(__FILE__),"lib")
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require 'patir/base'
6
+
7
+ Hoe.new('patir', "#{Patir::VERSION_MAJOR}.#{Patir::VERSION_MINOR}") do |p|
8
+ p.author = "Vassilis Rizopoulos"
9
+ p.email = "riva@braveworld.net"
10
+ p.summary = 'patir (Project Automation Tools in Ruby) provides libraries for use in automation tools'
11
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
12
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+ p.rubyforge_name="patir"
15
+ p.extra_deps<<['systemu']
16
+ end
17
+
18
+ # vim: syntax=Ruby
@@ -0,0 +1,46 @@
1
+ # Copyright (c) 2007 Vassilis Rizopoulos. All rights reserved.
2
+
3
+ #This is the base module of the Patir system. It contains some usefull helper methods used by all child projects.
4
+ module Patir
5
+ require 'logger'
6
+ #Version information
7
+ VERSION_MAJOR="0"
8
+ VERSION_MINOR="2"
9
+ VERSION="#{VERSION_MAJOR}.#{VERSION_MINOR}"
10
+ #Error thrown usually in initialize methods when missing required parameters
11
+ #from the initialization hash.
12
+ class ParameterException<RuntimeError
13
+ end
14
+ #creates a drb service URL from an ip and a port number
15
+ def self.drb_service ip,port
16
+ return "druby://#{ip}:#{port}"
17
+ end
18
+ #Just making Logger usage easier
19
+ #
20
+ #This is for use on top level scripts.
21
+ #
22
+ #It creates a logger just as we want it.
23
+ #
24
+ #mode can be
25
+ #
26
+ #:mute to set the level to FATAL
27
+ #
28
+ #:silent to set the level to WARN
29
+ #
30
+ #:debug to set the level to DEBUG. Debug is set also if $DEBUG is true.
31
+ #
32
+ #The default logger level is INFO
33
+ def self.setup_logger(filename=nil,mode=nil)
34
+ if filename
35
+ logger=Logger.new(filename)
36
+ else
37
+ logger=Logger.new(STDOUT)
38
+ end
39
+ logger.level=Logger::INFO
40
+ logger.level=Logger::FATAL if mode==:mute
41
+ logger.level=Logger::WARN if mode==:silent
42
+ logger.level=Logger::DEBUG if mode==:debug || $DEBUG
43
+ logger.datetime_format="%Y%m%d %H:%M:%S"
44
+ return logger
45
+ end
46
+ end
@@ -0,0 +1,486 @@
1
+ # Copyright (c) 2007 Vassilis Rizopoulos. All rights reserved.
2
+ require 'observer'
3
+ require 'fileutils'
4
+ require 'rubygems'
5
+ require 'systemu'
6
+ require 'patir/base'
7
+
8
+ module Patir
9
+ #This module defines the interface for a Command object.
10
+ #
11
+ #It more or less serves the purpose of documenting the interface/contract expected
12
+ #by a class that executes commands and returns their output and exit status.
13
+ #
14
+ #There is also that bit of functionality that facilitates grouping multiple commands into command sequences
15
+ #
16
+ #The various methods initialize member variables with meaningful values where needed.
17
+ #
18
+ #Using the contract means implementing the Command#run method. This method should then set
19
+ #the output, exec_time and status values according to the implementation.
20
+ #
21
+ #Take a look at ShellCommand and RubyCommand for a couple of practical examples.
22
+ #
23
+ #It is a good idea to rescue all exceptions. You can then set error to return the exception message.
24
+ module Command
25
+ attr_writer :output, :name, :exec_time,:error,:status
26
+ attr_accessor :number,:strategy
27
+ #returns the commands alias/name
28
+ def name
29
+ #initialize nil values to something meaningful
30
+ @name||=""
31
+ return @name
32
+ end
33
+ #returns the output of the command
34
+ def output
35
+ #initialize nil values to something meaningful
36
+ @output||=""
37
+ return @output
38
+ end
39
+ #returns the error output for the command
40
+ def error
41
+ #initialize nil values to something meaningful
42
+ @error||=""
43
+ return @error
44
+ end
45
+ #returns the execution time (duration) for the command
46
+ def exec_time
47
+ #initialize nil values to something meaningful
48
+ @exec_time||=0
49
+ return @exec_time
50
+ end
51
+ #returns true if the command has finished succesfully
52
+ def success?
53
+ return true if self.status==:success
54
+ return false
55
+ end
56
+ #returns true if the command has been executed
57
+ def run?
58
+ #use the accessor, because it initializes nil values
59
+ return false if self.status==:not_executed
60
+ return true
61
+ end
62
+ #executes the command and returns the status of the command.
63
+ #
64
+ #overwrite this method in classes that include Command
65
+ def run
66
+ @status=:success
67
+ return self.status
68
+ end
69
+ #clears the status and output of the command.
70
+ #
71
+ #Call this if you want to pretend that it was never executed
72
+ def reset
73
+ @exec_time=0
74
+ @output=""
75
+ @error=""
76
+ @status=:not_executed
77
+ end
78
+ #returns false if the command has not been run
79
+ def executed?
80
+ return false if self.status==:not_executed
81
+ return true
82
+ end
83
+ #returns the command status.
84
+ #
85
+ #valid stati are
86
+ #
87
+ #:not_executed when the command was not run
88
+ #
89
+ #:success when the command has finished succesfully
90
+ #
91
+ #:error when the command has an error
92
+ #
93
+ #:warning when the command finished without errors, but there where warnings
94
+ def status
95
+ #initialize nil values to something meaningful
96
+ @status||=:not_executed
97
+ return @status
98
+ end
99
+ end
100
+
101
+ #This class wraps the http://codeforpeople.com/lib/ruby/systemu/ as a Command duck.
102
+ #
103
+ #It allows for execution of any shell command.
104
+ #
105
+ #Accepted keys are
106
+ #
107
+ #:cmd should be the shell command to execute (required - ParameterException will be raised).
108
+ #
109
+ #:working_directory for specifying the working directory (default is '.') and :name for assigning a name to the command (default is "").
110
+ #
111
+ class ShellCommand
112
+ include Command
113
+ #The constructor will throw CommandError if :cmd is missing.
114
+ #
115
+ #CommandError will also be thrown if :working_directory does not exist.
116
+ def initialize *args
117
+ params=args[0] if args
118
+ raise ParameterException,"No ShellCommand parameters defined" unless params
119
+ @name=params[:name]
120
+ @working_directory=params[:working_directory]
121
+ #initialize the working directory value. This actually means ./
122
+ @working_directory||="."
123
+ #we need a command line :)
124
+ raise ParameterException,"No :command defined" unless params[:cmd]
125
+ @command=params[:cmd]
126
+ @status=:not_executed
127
+ end
128
+
129
+ #Executes the shell command and returns the status
130
+ def run
131
+ start_time=Time.now
132
+ super
133
+ #create the working directory if it does not exist
134
+ FileUtils::mkdir_p(@working_directory) unless File.exists?(@working_directory)
135
+ #create the actual command, run it, grab stderr and stdout and set output,error, status and execution time
136
+ status, @output, @error = systemu(@command,:cwd=>@working_directory)
137
+ #lets get the status how we want it
138
+ if status.exited?
139
+ if status.exitstatus==0
140
+ @status=:success
141
+ else
142
+ @status=:error
143
+ end
144
+ else
145
+ @status=:warning
146
+ end
147
+ #set the time it took us
148
+ @exec_time=Time.now-start_time
149
+ return @status
150
+ end
151
+
152
+ def to_s
153
+ return "#{@name}: #{@command} in #{File.expand_path(@working_directory)}"
154
+ end
155
+ end
156
+
157
+ #CommandSequence describes a set of commands to be executed in sequence.
158
+ #
159
+ #Each instance of CommandSequence contains a set of Patir::Command instances, which are the steps to perform.
160
+ #
161
+ #The steps are executed in the sequence they are added. A CommandSequence can terminate immediately on step failure or it can continue. It will still be marked as failed as long as a single step fails.
162
+ #
163
+ #Access to the CommandSequence status is achieved using the Observer pattern.
164
+ #
165
+ #The :sequence_status message contains the status of the sequence, an instance of the class CommandSequenceStatus.
166
+ #
167
+ #CommandSequence is designed to be reusable, so it does not correspond to a single sequence run, rather it corresponds to
168
+ #the currently active run. Calling reset, or run will discard the old state and create a new sequence 'instance' and status.
169
+ #
170
+ #No threads are spawned by CommandSequence (that doesn't mean the commands cannot, but it is not advisable).
171
+ class CommandSequence
172
+ include Observable
173
+ attr_reader :name,:state,:steps
174
+ attr_reader :sequence_runner
175
+ attr_reader :sequence_id
176
+
177
+ def initialize name,sequence_runner=""
178
+ @name=name
179
+ @steps||=Array.new
180
+ @sequence_runner=sequence_runner
181
+ #intialize the status for the currently active build (not executed)
182
+ reset
183
+ end
184
+
185
+ #sets the sequence runner attribute updating status
186
+ def sequence_runner=name
187
+ @sequence_runner=name
188
+ @state.sequence_runner=name
189
+ end
190
+
191
+ #sets the sequence id attribute updating status
192
+ def sequence_id=name
193
+ @sequence_id=name
194
+ @state.sequence_id=name
195
+ end
196
+ #Executes the CommandSequence.
197
+ #
198
+ #Will run all step instances in sequence observing the exit strategies on warning/failures
199
+ def run
200
+ #set the start time
201
+ @state.start_time=Time.now
202
+ #reset the stop time
203
+ @state.stop_time=nil
204
+ #we started running, lets tell the world
205
+ @state.status=:running
206
+ notify(:sequence_status=>@state)
207
+ #we are optimistic
208
+ running_status=:success
209
+ #but not that much
210
+ running_status=:warning if @steps.empty?
211
+ #execute the steps in sequence
212
+ @steps.each do |step|
213
+ #the step is running, tell the world
214
+ @state.step=step
215
+ step.status=:running
216
+ notify(:sequence_status=>@state)
217
+ #run it, get the result and notify
218
+ result=step.run
219
+ @state.step=step
220
+ step.status=:running
221
+ notify(:sequence_status=>@state)
222
+ #evaluate the results' effect on execution status at the end
223
+ case result
224
+ when :success
225
+ #everything is fine, continue
226
+ when :error
227
+ #this will be the final status
228
+ running_status=:error
229
+ #stop if we fail on error
230
+ if :fail_on_error==step.strategy
231
+ @state.status=:error
232
+ break
233
+ end
234
+ when :warning
235
+ #a previous failure overrides a warning
236
+ running_status=:warning unless :error==running_status
237
+ #escalate this to a failure if the strategy says so
238
+ running_status=:error if :flunk_on_warning==step.strategy
239
+ #stop if we fail on warning
240
+ if :fail_on_warning==step.strategy
241
+ @state.status=:error
242
+ break
243
+ end
244
+ end
245
+ end
246
+ #we finished
247
+ @state.stop_time=Time.now
248
+ @state.status=running_status
249
+ notify(:sequence_status=>@state)
250
+ end
251
+ #Adds a step to the CommandSequence using the given exit strategy.
252
+ #
253
+ #Steps are always added at the end of the build sequence. A step should quack like a Patir::Command.
254
+ #
255
+ #Valid exit strategies are
256
+ # :fail_on_error - CommandSequence terminates on failure of this step
257
+ # :flunk_on_error - CommandSequence is flagged as failed but continues to the next step
258
+ # :fail_on_warning - CommandSequence terminates on warnings in this step
259
+ # :flunk_on_warning - CommandSequence is flagged as failed on warning in this step
260
+ def add_step step,exit_strategy=:fail_on_error
261
+ #duplicate the command
262
+ bstep=step.dup
263
+ #reset it
264
+ bstep.reset
265
+ #set the extended attributes
266
+ bstep.number=@steps.size
267
+ bstep.strategy=exit_strategy
268
+ #add it to the lot
269
+ @steps<<bstep
270
+ #add it to status as well
271
+ @state.step=bstep
272
+ notify(:sequence_status=>@state)
273
+ end
274
+
275
+ #Resets the status. This will set :not_executed status,
276
+ #and set the start and end times to nil.
277
+ def reset
278
+ #reset all the steps (stati and execution times)
279
+ @steps.each{|step| step.reset}
280
+ #reset the status
281
+ @state=CommandSequenceStatus.new(@name)
282
+ @steps.each{|step| @state.step=step}
283
+ @state.start_time=Time.now
284
+ @state.stop_time=nil
285
+ @state.sequence_runner=@sequence_runner
286
+ #tell the world
287
+ notify(:sequence_status=>@state)
288
+ end
289
+
290
+ #Returns true if the sequence has finished executing
291
+ def completed?
292
+ return @state.completed?
293
+ end
294
+
295
+ def to_s
296
+ "#{sequence_id}:#{:name} on #{@sequence_runner}, #{@steps.size} steps"
297
+ end
298
+ private
299
+ #observer notification
300
+ def notify *params
301
+ changed
302
+ notify_observers(*params)
303
+ end
304
+ end
305
+
306
+ #CommandSequenceStatus represents the status of a CommandSequence, including the status of all the steps for this sequence.
307
+ #
308
+ #In order to extract the status from steps, classes should quack to the rythm of Command. CommandSequenceStatus does this, so you can nest Stati
309
+ #
310
+ #The status of an action sequence is :not_executed, :running, :success, :warning or :error and represents the overall status
311
+ #
312
+ #:not_executed is set when all steps are :not_executed
313
+ #
314
+ #:running is set while the sequence is running.
315
+ #
316
+ #Upon completion or interruption one of :success, :error or :warning will be set.
317
+ #
318
+ #:success is set when all steps are succesfull.
319
+ #
320
+ #:warning is set when at least one step generates warnings and there are no failures.
321
+ #
322
+ #:error is set when after execution at least one step has the :error status
323
+ class CommandSequenceStatus
324
+ attr_accessor :start_time,:stop_time,:sequence_runner,:sequence_name,:status,:step_states,:sequence_id
325
+ #You can pass an array of Commands to initialize CommandSequenceStatus
326
+ def initialize sequence_name,steps=nil
327
+ @sequence_name=sequence_name
328
+ @sequence_runner=""
329
+ @sequence_id=nil
330
+ @step_states||=Hash.new
331
+ #not run yet
332
+ @status=:not_executed
333
+ #translate the array of steps as we need it in number=>state form
334
+ steps.each{|step| self.step=step } if steps
335
+ @start_time=Time.now
336
+ end
337
+ def running?
338
+ return true if :running==@status
339
+ return false
340
+ end
341
+ #true is returned when all steps were succesfull.
342
+ def success?
343
+ return true if :success==@status
344
+ return false
345
+ end
346
+
347
+ #A sequence is considered completed when:
348
+ #
349
+ #a step has errors and the :fail_on_error strategy is used
350
+ #
351
+ #a step has warnings and the :fail_on_warning strategy is used
352
+ #
353
+ #in all other cases if any steps have :not_executed or :running status
354
+ def completed?
355
+ #this saves us iterating once+1 when no execution took place
356
+ return false if !self.executed?
357
+ @step_states.each do |state|
358
+ return true if state[1][:status]==:error && state[1][:strategy]==:fail_on_error
359
+ return true if state[1][:status]==:warning && state[1][:strategy]==:fail_on_warning
360
+ end
361
+ @step_states.each{|state| return false if state[1][:status]==:not_executed || state[1][:status]==:running }
362
+ return true
363
+ end
364
+ #A nil means there is no step with that number
365
+ def step_state number
366
+ s=@step_states[number] if @step_states[number]
367
+ return s
368
+ end
369
+ #Adds a step to the state. The step state is inferred from the Command instance __step__
370
+ def step=step
371
+ @step_states[step.number]={:name=>step.name,
372
+ :status=>step.status,
373
+ :output=>step.output,
374
+ :duration=>step.exec_time,
375
+ :error=>step.error,
376
+ :strategy=>step.strategy}
377
+ #this way we don't have to compare all the step states we always get the worst last stable state
378
+ #:not_executed<:success<:warning<:success
379
+ @previous_status=@status unless @status==:running
380
+ case step.status
381
+ when :running
382
+ @status=:running
383
+ when :warning
384
+ @status=:warning unless @status==:error
385
+ @status=:error if @previous_status==:error
386
+ when :error
387
+ @status=:error
388
+ when :success
389
+ @status=:success unless @status==:error || @status==:warning
390
+ @status=:warning if @previous_status==:warning
391
+ @status=:error if @previous_status==:error
392
+ when :not_executed
393
+ @status=@previous_status
394
+ end
395
+ end
396
+ #produces a brief text summary for this status
397
+ def summary
398
+ sum=""
399
+ sum<<"#{@sequence_id}:" if @sequence_id
400
+ sum<<"#{@sequence_name}. " unless @sequence_name.empty?
401
+ sum<<"Status - #{@status}"
402
+ if !@step_states.empty?
403
+ sum<<".States #{@step_states.size}\nStep status summary:"
404
+ if :not_executed!=@status
405
+ @step_states.each do |number,state|
406
+ sum<<"\n\t#{number}:'#{state[:name]}' - #{state[:status]}"
407
+ end
408
+ end
409
+ end
410
+ return sum
411
+ end
412
+ def to_s
413
+ "'#{sequence_id}':'#{@sequence_name}' on '#{@sequence_runner}' started at #{@start_time}.#{@step_states.size} steps"
414
+ end
415
+ def exec_time
416
+ return @stop_time-@start_time if @stop_time
417
+ return 0
418
+ end
419
+ def name
420
+ return @sequence_name
421
+ end
422
+ def number
423
+ return @sequence_id
424
+ end
425
+ def output
426
+ return self.summary
427
+ end
428
+ def error
429
+ return ""
430
+ end
431
+ def executed?
432
+ return true unless @status==:not_executed
433
+ return false
434
+ end
435
+ end
436
+
437
+ #This class allows you to wrap Ruby blocks and handle them like Command
438
+ #
439
+ #Provide a block to RubyCommand#new and you can execute the block using
440
+ #RubyCommand#run
441
+ #
442
+ #The block receives the instance of RubyCommand so you can set the output and error output.
443
+ #
444
+ #The return value of the block is assigned as the command status.
445
+ #
446
+ #== Examples
447
+ #An example (using the excellent HighLine lib) of a CLI prompt as a RubyCommand
448
+ # RubyCommand.new("prompt") do |cmd|
449
+ # cmd.output=""
450
+ # cmd.error=""
451
+ # if HighLine.agree("#{step.text}?")
452
+ # :success
453
+ # else
454
+ # :error
455
+ # end
456
+ # end
457
+ class RubyCommand
458
+ include Patir::Command
459
+ def initialize name,working_directory=nil,&block
460
+ @name=name
461
+ @working_directory=working_directory
462
+ if block_given?
463
+ @cmd=block
464
+ else
465
+ raise "You need to provide a block"
466
+ end
467
+ end
468
+ #Runs the associated block
469
+ def run
470
+ @run=true
471
+ prev_dir=Dir.pwd
472
+ begin
473
+ Dir.chdir(@working_directory) if @working_directory
474
+ t1=Time.now
475
+ @status=@cmd.call(self)
476
+ @exec_time=Time.now-t1
477
+ rescue SystemCallError
478
+ @output="#{$!}"
479
+ @success=false
480
+ ensure
481
+ Dir.chdir(prev_dir)
482
+ end
483
+ return @success
484
+ end
485
+ end
486
+ end