patir 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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