Package qm :: Package test :: Module target
[hide private]
[frames] | no frames]

Source Code for Module qm.test.target

  1  ######################################################################## 
  2  # 
  3  # File:   target.py 
  4  # Author: Mark Mitchell 
  5  # Date:   10/29/2001 
  6  # 
  7  # Contents: 
  8  #   QMTest Target class. 
  9  # 
 10  # Copyright (c) 2001, 2002, 2003 by CodeSourcery, LLC.  All rights reserved.  
 11  # 
 12  # For license terms see the file COPYING. 
 13  # 
 14  ######################################################################## 
 15   
 16  ######################################################################## 
 17  # imports 
 18  ######################################################################## 
 19   
 20  import qm 
 21  import qm.common 
 22  import qm.extension 
 23  import qm.platform 
 24  import qm.test.base 
 25  from   qm.test.context import * 
 26  from   qm.test.result import * 
 27  from   qm.test.database import NoSuchResourceError 
 28  import re 
 29  import signal 
 30  import sys 
 31   
 32  ######################################################################## 
 33  # classes 
 34  ######################################################################## 
 35   
36 -class Target(qm.extension.Extension):
37 """Base class for target implementations. 38 39 A 'Target' is an entity that can run tests. QMTest can spread the 40 workload from multiple tests across multiple targets. In 41 addition, a single target can run more than one test at once. 42 43 'Target' is an abstract class. 44 45 You can extend QMTest by providing your own target class 46 implementation. 47 48 To create your own target class, you must create a Python class 49 derived (directly or indirectly) from 'Target'. The documentation 50 for each method of 'Target' indicates whether you must override it 51 in your target class implementation. Some methods may be 52 overridden, but do not need to be. You might want to override 53 such a method to provide a more efficient implementation, but 54 QMTest will work fine if you just use the default version.""" 55 56 arguments = [ 57 qm.fields.TextField( 58 name="name", 59 title="Name", 60 description="""The name of this target. 61 62 The name of the target. The target name will be recorded 63 in any tests executed on that target so that you can see 64 where the test was run.""", 65 default_value=""), 66 qm.fields.TextField( 67 name="group", 68 title="Group", 69 description="""The group associated with this target. 70 71 Some tests may only be able to run on some targets. A 72 test can specify a pattern indicating the set of targets 73 on which it will run.""", 74 default_value="") 75 ] 76 77 kind = "target" 78
79 - class __ResourceSetUpException(Exception):
80 """An exception indicating that a resource could not be set up.""" 81
82 - def __init__(self, resource):
83 """Construct a new 'ResourceSetUpException'. 84 85 'resource' -- The name of the resoure that could not be 86 set up.""" 87 88 self.resource = resource
89 90 91
92 - def __init__(self, database, arguments = None, **args):
93 """Construct a 'Target'. 94 95 'database' -- The 'Database' containing the tests that will be 96 run. 97 98 'arguments' -- As for 'Extension.__init__'. 99 100 'args' -- As for 'Extension.__init__'.""" 101 102 if arguments: args.update(arguments) 103 super(Target, self).__init__(**args) 104 105 self.__database = database
106 107
108 - def GetName(self):
109 """Return the name of the target. 110 111 Derived classes must not override this method.""" 112 113 return self.name
114 115
116 - def GetGroup(self):
117 """Return the group of which the target is a member. 118 119 Derived classes must not override this method.""" 120 121 return self.group
122 123
124 - def GetDatabase(self):
125 """Return the 'Database' containing the tests this target will run. 126 127 returns -- The 'Database' containing the tests this target will 128 run. 129 130 Derived classes must not override this method.""" 131 132 return self.__database
133 134
135 - def IsIdle(self):
136 """Return true if the target is idle. 137 138 returns -- True if the target is idle. If the target is idle, 139 additional tasks may be assigned to it. 140 141 Derived classes must override this method.""" 142 143 raise NotImplementedError
144 145
146 - def IsInGroup(self, group_pattern):
147 """Returns true if this 'Target' is in a particular group. 148 149 'group_pattern' -- A string giving a regular expression. 150 151 returns -- Returns true if the 'group_pattern' denotes a 152 regular expression that matches the group for this 'Target', 153 false otherwise.""" 154 155 return re.match(group_pattern, self.GetGroup())
156 157
158 - def Start(self, response_queue, engine=None):
159 """Start the target. 160 161 'response_queue' -- The 'Queue' in which the results of test 162 executions are placed. 163 164 'engine' -- The 'ExecutionEngine' that is starting the target, 165 or 'None' if this target is being started without an 166 'ExecutionEngine'. 167 168 Derived classes may override this method, but the overriding 169 method must call this method at some point during its 170 execution.""" 171 172 self.__response_queue = response_queue 173 self.__engine = engine 174 # There are no resources available on this target yet. 175 self.__resources = {} 176 self.__order_of_resources = []
177 178
179 - def Stop(self):
180 """Stop the target. 181 182 Clean up all resources that have been set up on this target 183 and take whatever other actions are required to stop the 184 target. 185 186 Derived classes may override this method.""" 187 188 # Clean up any available resources. 189 self.__order_of_resources.reverse() 190 for name in self.__order_of_resources: 191 rop = self.__resources[name] 192 if rop and rop[1] == Result.PASS: 193 self._CleanUpResource(name, rop[0]) 194 del self.__response_queue 195 del self.__engine 196 del self.__resources 197 del self.__order_of_resources
198 199
200 - def RunTest(self, descriptor, context):
201 """Run the test given by 'test_id'. 202 203 'descriptor' -- The 'TestDescriptor' for the test. 204 205 'context' -- The 'Context' in which to run the test. 206 207 Derived classes may override this method.""" 208 209 # Create the result. 210 result = Result(Result.TEST, descriptor.GetId()) 211 try: 212 # Augment the context appropriately. 213 context = Context(context) 214 context[context.TARGET_CONTEXT_PROPERTY] = self.GetName() 215 context[context.TMPDIR_CONTEXT_PROPERTY] \ 216 = self._GetTemporaryDirectory() 217 context[context.DB_PATH_CONTEXT_PROPERTY] \ 218 = descriptor.GetDatabase().GetPath() 219 # Set up any required resources. 220 self.__SetUpResources(descriptor, context) 221 # Make the ID of the test available. 222 context[context.ID_CONTEXT_PROPERTY] = descriptor.GetId() 223 # Note the start time. 224 result[Result.START_TIME] = qm.common.format_time_iso() 225 # Run the test. 226 try: 227 descriptor.Run(context, result) 228 except qm.common.Timeout, t: 229 result.SetOutcome(result.FAIL, 'Process timed out.') 230 result[Result.TIMEOUT_DETAIL] = str(t) 231 finally: 232 # Note the end time. 233 result[Result.END_TIME] = qm.common.format_time_iso() 234 except KeyboardInterrupt: 235 result.NoteException(cause = "Interrupted by user.") 236 # We received a KeyboardInterrupt, indicating that the 237 # user would like to exit QMTest. Ask the execution 238 # engine to stop. 239 if self.__engine: 240 self.__engine.RequestTermination() 241 except qm.platform.SignalException, e: 242 # Note the exception. 243 result.NoteException(cause = str(e)) 244 # If we get a SIGTERM, propagate it so that QMTest 245 # terminates. 246 if e.GetSignalNumber() == signal.SIGTERM: 247 # Record the result so that the traceback is 248 # available. 249 self._RecordResult(result) 250 # Ask the execution engine to stop running tests. 251 if self.__engine: 252 self.__engine.RequestTermination() 253 # Re-raise the exception. 254 raise 255 except self.__ResourceSetUpException, e: 256 result.SetOutcome(Result.UNTESTED) 257 result[Result.CAUSE] = qm.message("failed resource") 258 result[Result.RESOURCE] = e.resource 259 except: 260 result.NoteException() 261 # Record the result. 262 self._RecordResult(result)
263 264
265 - def _RecordResult(self, result):
266 """Record the 'result'. 267 268 'result' -- A 'Result' of a test or resource execution. 269 270 Derived classes may override this method, but the overriding 271 method must call this method at some point during its 272 execution.""" 273 274 # Record the target in the result. 275 result[Result.TARGET] = self.GetName() 276 # Put the result into the response queue. 277 self.__response_queue.put(result)
278 279
280 - def _BeginResourceSetUp(self, resource_name):
281 """Begin setting up the indicated resource. 282 283 'resource_name' -- A string naming a resource. 284 285 returns -- If at attempt to set up the resource has already 286 been made, returns a tuple '(resource, outcome, properties)'. 287 The 'resource' is the 'Resource' object itself, but may be 288 'None' if the resource could not be set up. The 'outcome' 289 indicates the outcome that resulted when the resource was set 290 up. The 'properties' are a map from strings to strings 291 indicating properties added by this resource. 292 293 If the resource has not been set up, but _BeginResourceSetUp 294 has already been called for the resource, then the contents of 295 the tuple will all be 'None'. 296 297 If this is the first time _BeginResourceSetUp has been called 298 for this resource, then 'None' is returned, but the resource 299 is marked as in the process of being set up. It is the 300 caller's responsibility to finish setting it up by calling 301 '_FinishResourceSetUp'.""" 302 303 rop = self.__resources.get(resource_name) 304 if rop: 305 return rop 306 self.__resources[resource_name] = (None, None, None) 307 return None
308 309
310 - def _FinishResourceSetUp(self, resource, result, properties):
311 """Finish setting up a resource. 312 313 'resource' -- The 'Resource' itself. 314 315 'result' -- The 'Result' associated with setting up the 316 resource. 317 318 'properties' -- A dictionary of additional context properties 319 that should be provided to tests that depend on this resource. 320 321 returns -- A tuple of the same form as is returned by 322 '_BeginResourceSetUp' when the resource has already been set 323 up.""" 324 325 # The temporary directory is not be preserved; there is no 326 # guarantee that it will be the same in a test that depends on 327 # this resource as it was in the resource itself. 328 del properties[Context.TMPDIR_CONTEXT_PROPERTY] 329 rop = (resource, result.GetOutcome(), properties) 330 self.__resources[result.GetId()] = rop 331 self.__order_of_resources.append(result.GetId()) 332 return rop
333 334
335 - def __SetUpResources(self, descriptor, context):
336 """Set up all the resources associated with 'descriptor'. 337 338 'descriptor' -- The 'TestDescriptor' or 'ResourceDescriptor' 339 indicating the test or resource that is about to be run. 340 341 'context' -- The 'Context' in which the resources will be 342 executed. 343 344 returns -- A tuple of the same form as is returned by 345 '_BeginResourceSetUp' when the resource has already been set 346 up.""" 347 348 # See if there are resources that need to be set up. 349 for resource in descriptor.GetResources(): 350 (r, outcome, resource_properties) \ 351 = self._SetUpResource(resource, context) 352 353 # If the resource was not set up successfully, 354 # indicate that the test itself could not be run. 355 if outcome != Result.PASS: 356 raise self.__ResourceSetUpException, resource 357 # Update the list of additional context properties. 358 context.update(resource_properties) 359 360 return context
361 362
363 - def _SetUpResource(self, resource_name, context):
364 """Set up the resource given by 'resource_id'. 365 366 'resource_name' -- The name of the resource to be set up. 367 368 'context' -- The 'Context' in which to run the resource. 369 370 returns -- A map from strings to strings indicating additional 371 properties added by this resource.""" 372 373 # Begin setting up the resource. 374 rop = self._BeginResourceSetUp(resource_name) 375 # If it has already been set up, there is no need to do it 376 # again. 377 if rop: 378 return rop 379 # Set up the context. 380 wrapper = Context(context) 381 result = Result(Result.RESOURCE_SETUP, resource_name, Result.PASS) 382 resource = None 383 # Get the resource descriptor. 384 try: 385 resource_desc = self.GetDatabase().GetResource(resource_name) 386 # Set up the resources on which this resource depends. 387 self.__SetUpResources(resource_desc, wrapper) 388 # Make the ID of the resource available. 389 wrapper[Context.ID_CONTEXT_PROPERTY] = resource_name 390 # Set up the resource itself. 391 try: 392 resource_desc.SetUp(wrapper, result) 393 finally: 394 del wrapper[Context.ID_CONTEXT_PROPERTY] 395 # Obtain the resource within the try-block so that if it 396 # cannot be obtained the exception is handled below. 397 resource = resource_desc.GetItem() 398 except self.__ResourceSetUpException, e: 399 result.Fail(qm.message("failed resource"), 400 { result.RESOURCE : e.resource }) 401 except NoSuchResourceError: 402 result.NoteException(cause="Resource is missing from the database.") 403 self._RecordResult(result) 404 return (None, result, None) 405 except qm.test.base.CouldNotLoadExtensionError, e: 406 result.NoteException(e.exc_info, 407 cause = "Could not load extension class") 408 except KeyboardInterrupt: 409 result.NoteException() 410 # We received a KeyboardInterrupt, indicating that the 411 # user would like to exit QMTest. Ask the execution 412 # engine to stop. 413 if self.__engine: 414 self.__engine.RequestTermination() 415 except: 416 result.NoteException() 417 # Record the result. 418 self._RecordResult(result) 419 # And update the table of available resources. 420 return self._FinishResourceSetUp(resource, result, 421 wrapper.GetAddedProperties())
422 423
424 - def _CleanUpResource(self, name, resource):
425 """Clean up the 'resource'. 426 427 'resource' -- The 'Resource' that should be cleaned up. 428 429 'name' -- The name of the resource itself.""" 430 431 result = Result(Result.RESOURCE_CLEANUP, name) 432 # Clean up the resource. 433 try: 434 val = resource.CleanUp(result) 435 except: 436 result.NoteException() 437 self._RecordResult(result)
438 439
440 - def _GetTemporaryDirectory(self):
441 """Return the path to a temporary directory. 442 443 returns -- The path to a temporary directory to pass along to 444 tests and resources via the 'TMPDIR_CONTEXT_PROPERTY'.""" 445 446 raise NotImplementedError
447