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

Source Code for Module qm.test.base

  1  ######################################################################## 
  2  # 
  3  # File:   base.py 
  4  # Author: Alex Samuel 
  5  # Date:   2001-03-08 
  6  # 
  7  # Contents: 
  8  #   Base interfaces and classes. 
  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 cPickle 
 21  import cStringIO 
 22  import os 
 23  import qm 
 24  import qm.attachment 
 25  from   qm.common import * 
 26  from   qm.test.file_result_reader import FileResultReader 
 27  from   qm.test.expectation_database import ExpectationDatabase 
 28  import qm.platform 
 29  import qm.structured_text 
 30  from   qm.test.context import * 
 31  from   qm.test.result import * 
 32  import qm.xmlutil 
 33  import string 
 34  import sys 
 35  import tempfile 
 36  import types 
 37   
 38  ######################################################################## 
 39  # Exceptions 
 40  ######################################################################## 
 41   
42 -class CouldNotLoadExtensionError(QMException):
43 """An exception indicating that an extension class could not be loaded.""" 44
45 - def __init__(self, class_name, exc_info):
46 """Construct a new 'CouldNotLoadExtensionError'. 47 48 'class_name' -- The name of the class. 49 50 'exc_info' -- An exception tuple, as returned by 'sys.exc_info'.""" 51 52 self.exc_info = exc_info 53 message = qm.common.format_exception(exc_info) 54 message += "\n" + qm.error("could not load extension class", 55 class_name = class_name) 56 QMException.__init__(self, message)
57 58 ######################################################################## 59 # Functions 60 ######################################################################## 61
62 -def get_extension_directories(kind, database, database_path = None):
63 """Return the directories to search for QMTest extensions. 64 65 'kind' -- A string giving kind of extension for which we are looking. 66 This must be of the elements of 'extension_kinds'. 67 68 'database' -- The 'Database' with which the extension class will be 69 used, or 'None'. 70 71 'database_path' -- The path from which the database will be loaded. 72 If 'None', 'database.GetPath()' is used. 73 74 returns -- A sequence of strings. Each string is the path to a 75 directory that should be searched for QMTest extensions. The 76 directories must be searched in order; the first directory 77 containing the desired module is the one from which the module is 78 loaded. 79 80 The directories that are returned are, in order: 81 82 1. Those directories present in the 'QMTEST_CLASS_PATH' environment 83 variable. 84 85 2. Those directories specified by the 'GetClassPaths' method on the 86 test database -- unless 'kind' is 'database'. 87 88 3. The directory specified by config.extension_path. 89 90 4. The directories containing classes that come with QMTest. 91 92 By placing the 'QMTEST_CLASS_PATH' directories first, users can 93 override test classes with standard names.""" 94 95 global extension_kinds 96 97 # The kind should be one of the extension_kinds. 98 assert kind in extension_kinds 99 100 # Start with the directories that the user has specified in the 101 # QMTEST_CLASS_PATH environment variable. 102 if os.environ.has_key('QMTEST_CLASS_PATH'): 103 dirs = string.split(os.environ['QMTEST_CLASS_PATH'], 104 os.pathsep) 105 else: 106 dirs = [] 107 108 # Search directories specified by the database. 109 if database: 110 dirs = dirs + database.GetClassPaths() 111 112 # Search the database configuration directory. 113 if database: 114 dirs.append(database.GetConfigurationDirectory()) 115 elif database_path: 116 dirs.append(qm.test.database.get_configuration_directory 117 (database_path)) 118 119 # Search qmtest's own site-extensions directory. 120 dirs.append(os.path.join(qm.prefix, qm.extension_path)) 121 122 dirs.append(qm.common.get_lib_directory('test', 'classes')) 123 124 return dirs
125 126
127 -def get_extension_class_names_in_directory(directory):
128 """Return the names of QMTest extension classes in 'directory'. 129 130 'directory' -- A string giving the path to a directory in the file 131 system. 132 133 returns -- A dictionary mapping the strings in 'extension_kinds' to 134 sequences of strings. Each element in the sequence names an 135 extension class, using the form 'module.class'""" 136 137 global extension_kinds 138 139 # Assume that there are no extension classes in this directory. 140 extensions = {} 141 for kind in extension_kinds: 142 extensions[kind] = [] 143 144 # Look for a file named 'classes.qmc' in this directory. 145 file = os.path.join(directory, 'classes.qmc') 146 # If the file does not exist, there are no extension classes in 147 # this directory. 148 if not os.path.isfile(file): 149 return extensions 150 151 try: 152 # Load the file. 153 document = qm.xmlutil.load_xml_file(file) 154 # Get the root node in the document. 155 root = document.documentElement 156 # Get the sequence of elements corresponding to each of the 157 # classes listed in the directory. 158 classes = root.getElementsByTagName("class") 159 # Go through each of the classes to see what kind it is. 160 for c in classes: 161 kind = c.getAttribute('kind') 162 # Skip extensions we do not understand. Perhaps they 163 # are for some other QM tool. 164 if kind not in extension_kinds: 165 continue 166 if c.hasAttribute("name"): 167 name = c.getAttribute("name") 168 else: 169 # Before QMTest 2.1, the class name was contained in 170 # the class element, rather than being an attribute. 171 name = qm.xmlutil.get_dom_text(c) 172 # Strip whitespace. 173 name = name.strip() 174 extensions[kind].append(name) 175 except: 176 raise 177 178 return extensions
179 180
181 -def get_extension_class_names(kind, database, database_path = None):
182 """Return the names of extension classes. 183 184 'kind' -- The kind of extension class. This value must be one 185 of the 'extension_kinds'. 186 187 'database' -- The 'Database' with which the extension class will be 188 used, or 'None' if 'kind' is 'database'. 189 190 'database_path' -- The path from which the database will be loaded. 191 If 'None', 'database.GetPath()' is used. 192 193 returns -- A sequence of strings giving the names of the extension 194 classes with the indicated 'kind', in the form 'module.class'.""" 195 196 dirs = get_extension_directories(kind, database, database_path) 197 names = [] 198 for d in dirs: 199 names.extend(get_extension_class_names_in_directory(d)[kind]) 200 return names
201 202
203 -def get_extension_class_from_directory(class_name, kind, directory, path):
204 """Load an extension class from 'directory'. 205 206 'class_name' -- The name of the extension class, in the form 207 'module.class'. 208 209 'kind' -- The kind of class to load. This value must be one 210 of the 'extension_kinds'. 211 212 'directory' -- The directory from which to load the class. 213 214 'path' -- The directories to search for modules imported by the new 215 module. 216 217 returns -- The class loaded.""" 218 219 global __class_caches 220 global __extension_bases 221 222 # If this class is already in the cache, we can just return it. 223 cache = __class_caches[kind] 224 if cache.has_key(class_name): 225 return cache[class_name] 226 227 # Load the class. 228 try: 229 klass = qm.common.load_class(class_name, [directory], 230 path + sys.path) 231 except: 232 raise CouldNotLoadExtensionError(class_name, sys.exc_info()) 233 234 # Make sure the class is derived from the appropriate base class. 235 if not issubclass(klass, __extension_bases[kind]): 236 raise QMException, \ 237 qm.error("extension class not subclass", 238 kind = kind, 239 class_name = class_name, 240 base_name = __extension_bases[kind].__name__) 241 242 # Cache it. 243 cache[class_name] = klass 244 245 return klass
246 247
248 -def get_extension_class(class_name, kind, database, database_path = None):
249 """Return the extension class named 'class_name'. 250 251 'class_name' -- The name of the class, in the form 'module.class'. 252 253 'kind' -- The kind of class to load. This value must be one 254 of the 'extension_kinds'. 255 256 'database' -- The 'Database' with which the extension class will be 257 used, or 'None' if 'kind' is 'database'. 258 259 'database_path' -- The path from which the database will be loaded. 260 If 'None', 'database.GetPath()' is used. 261 262 returns -- The class object with the indicated 'class_name'.""" 263 264 global __class_caches 265 266 # If this class is already in the cache, we can just return it. 267 cache = __class_caches[kind] 268 if cache.has_key(class_name): 269 return cache[class_name] 270 271 # For backwards compatibility with QM 1.1.x, we accept 272 # "xmldb.Database" and "qm.test.xmldb.Database", even though those 273 # to do not name actual database classes any more. 274 if kind == "database" and class_name in ("xmldb.Database", 275 "qm.test.xmldb.Database"): 276 class_name = "xml_database.XMLDatabase" 277 278 # Look for the class in each of the extension directories. 279 directories = get_extension_directories(kind, database, database_path) 280 directory = None 281 for d in directories: 282 if class_name in get_extension_class_names_in_directory(d)[kind]: 283 directory = d 284 break 285 286 # If the class could not be found, issue an error. 287 if not directory: 288 raise QMException, qm.error("extension class not found", 289 klass=class_name) 290 291 # Load the class. 292 return get_extension_class_from_directory(class_name, kind, 293 directory, directories)
294 295
296 -def get_test_class(class_name, database):
297 """Return the test class named 'class_name'. 298 299 'class_name' -- The name of the test class, in the form 300 'module.class'. 301 302 returns -- The test class object with the indicated 'class_name'.""" 303 304 return get_extension_class(class_name, 'test', database)
305 306
307 -def get_resource_class(class_name, database):
308 """Return the resource class named 'class_name'. 309 310 'class_name' -- The name of the resource class, in the form 311 'module.class'. 312 313 returns -- The resource class object with the indicated 314 'class_name'.""" 315 316 return get_extension_class(class_name, 'resource', database)
317 318
319 -def get_extension_classes(kind, database = None):
320 """Return the extension classes for the given 'kind'. 321 322 'kind' -- The kind of extensions being sought. The value must be 323 one of the 'extension_kinds'. 324 325 'database' -- If not 'None', the test 'Database' in use. 326 327 returns -- A list of the available extension classes of the 328 indicated 'kind'.""" 329 330 classes = [] 331 directories = get_extension_directories(kind, database) 332 for d in directories: 333 names = get_extension_class_names_in_directory(d)[kind] 334 d_classes = [get_extension_class_from_directory(n, kind, d, 335 directories) 336 for n in names] 337 classes.extend(d_classes) 338 339 return classes
340 341
342 -def load_results(file, database):
343 """Read test results from a file. 344 345 'file' -- The filename or file object from which to read the 346 results. If 'file' is not a string, then it is must be a seekable 347 file object, and this function will look for a 'FileResultReader' 348 that accepts the file. If 'file' is a string, then it is treated as 349 either a filename or as an extension descriptor. 350 351 'database' -- The current database. 352 353 returns -- A 'ResultReader' object, or raises an exception if no 354 appropriate reader is available.""" 355 356 f = None 357 if isinstance(file, types.StringTypes): 358 if os.path.exists(file): 359 f = open(file, "rb") 360 else: 361 f = file 362 if f: 363 # Find the first FileResultStream that will accept this file. 364 for c in get_extension_classes("result_reader", database): 365 if issubclass(c, FileResultReader): 366 try: 367 return c({"file" : f}) 368 except FileResultReader.InvalidFile: 369 # Go back to the beginning of the file. 370 f.seek(0) 371 if not isinstance(file, types.StringTypes): 372 raise FileResultReader.InvalidFile, \ 373 "not a valid results file" 374 if database: 375 extension_loader = database.GetExtension 376 else: 377 extension_loader = None 378 class_loader = lambda n: get_extension_class(n, 379 "result_reader", 380 database) 381 cl, args = qm.extension.parse_descriptor(file, 382 class_loader, 383 extension_loader) 384 return cl(args)
385 386
387 -def load_expectations(file, database, annotations = None):
388 """Read expectations from a file. 389 390 'file' -- The filename or file object from which to read the 391 expectations. If 'file' is not a string, then it is must be a seekable 392 file object, and this function will look for an 'ExpectationDatabase' 393 that accepts the file. If 'file' is a string, then it is treated as 394 either a filename or as an extension descriptor. 395 396 'database' -- The current database. 397 398 'annotations' -- Annotations for the current test run. 399 400 returns -- An 'ExpectationDatabase' object, or raises an exception if no 401 appropriate reader is available.""" 402 403 if not file: 404 return ExpectationDatabase() 405 f = None 406 if isinstance(file, (str, unicode)): 407 if os.path.exists(file): 408 f = open(file, "rb") 409 else: 410 f = file 411 if f: 412 return PreviousTestRun(test_database = database, results_file = f) 413 if not isinstance(file, (str, unicode)): 414 raise QMException, "not a valid expectation database" 415 if database: 416 extension_loader = database.GetExtension 417 else: 418 extension_loader = None 419 class_loader = lambda n: get_extension_class(n, 420 "expectation_database", 421 database) 422 cl, args = qm.extension.parse_descriptor(file, 423 class_loader, 424 extension_loader) 425 args['test_database'] = database 426 args['testrun_parameters'] = annotations or {} 427 return cl(**args)
428 429
430 -def load_outcomes(file, database):
431 """Load test outcomes from a file. 432 433 'file' -- The file object from which to read the results. See 434 'load_results' for details. 435 436 'database' -- The current database. 437 438 returns -- A map from test IDs to outcomes.""" 439 440 results = load_results(file, database) 441 outcomes = {} 442 for r in results: 443 # Keep test outcomes only. 444 if r.GetKind() == Result.TEST: 445 outcomes[r.GetId()] = r.GetOutcome() 446 return outcomes
447 448
449 -def _result_from_dom(node):
450 """Extract a result from a DOM node. 451 452 'node' -- A DOM node corresponding to a "result" element. 453 454 returns -- A 'Result' object. The context for the result is 'None', 455 since context is not represented in a result DOM node.""" 456 457 assert node.tagName == "result" 458 # Extract the outcome. 459 outcome = qm.xmlutil.get_child_text(node, "outcome") 460 # Extract the test ID. 461 test_id = node.getAttribute("id") 462 kind = node.getAttribute("kind") 463 # Build a Result. 464 result = Result(kind, test_id, outcome) 465 # Extract properties, one for each property element. 466 for property_node in node.getElementsByTagName("property"): 467 # The name is stored in an attribute. 468 name = property_node.getAttribute("name") 469 # The value is stored in the child text node. 470 value = qm.xmlutil.get_dom_text(property_node) 471 # Store it. 472 result[name] = value 473 474 return result
475 476 477 ######################################################################## 478 # variables 479 ######################################################################## 480 481 import qm.test.database 482 import qm.label 483 import qm.host 484 import qm.test.resource 485 import qm.test.result_reader 486 import qm.test.result_stream 487 import qm.test.run_database 488 import qm.test.expectation_database 489 import qm.test.suite 490 import qm.test.target 491 import qm.test.test 492 from qm.test.classes.previous_testrun import PreviousTestRun 493 494 __extension_bases = { 495 'database' : qm.test.database.Database, 496 'host' : qm.host.Host, 497 'label' : qm.label.Label, 498 'resource' : qm.test.resource.Resource, 499 'result_reader' : qm.test.result_reader.ResultReader, 500 'result_stream' : qm.test.result_stream.ResultStream, 501 'run_database' : qm.test.run_database.RunDatabase, 502 'expectation_database' : qm.test.expectation_database.ExpectationDatabase, 503 'suite' : qm.test.suite.Suite, 504 'target' : qm.test.target.Target, 505 'test' : qm.test.test.Test 506 } 507 """A map from extension class kinds to base classes. 508 509 An extension class of a particular 'kind' must be derived from 510 'extension_bases[kind]'.""" 511 512 extension_kinds = __extension_bases.keys() 513 """Names of different kinds of QMTest extension classes.""" 514 extension_kinds.sort() 515 516 __class_caches = {} 517 """A dictionary of loaded class caches. 518 519 The keys are the kinds in 'extension_kinds'. The associated value 520 is itself a dictionary mapping class names to class objects.""" 521 522 # Initialize the caches. 523 for kind in extension_kinds: 524 __class_caches[kind] = {} 525 526 ######################################################################## 527 # Local Variables: 528 # mode: python 529 # indent-tabs-mode: nil 530 # fill-column: 72 531 # End: 532