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

Source Code for Module qm.test.database

   1  ######################################################################## 
   2  # 
   3  # File:   database.py 
   4  # Author: Mark Mitchell 
   5  # Date:   2001-10-05 
   6  # 
   7  # Contents: 
   8  #   QMTest database 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 os.path 
  21  import qm 
  22  from   qm.common import * 
  23  import qm.extension 
  24  import qm.fields 
  25  from   qm.label import * 
  26  from   qm.test.base import * 
  27  from   qm.test.directory_suite import DirectorySuite 
  28  from   qm.test.runnable import Runnable 
  29  from   qm.test.resource import Resource 
  30  from   qm.test.suite import Suite 
  31  from   qm.test.test import Test 
  32   
  33  ######################################################################## 
  34  # Variables 
  35  ######################################################################## 
  36   
  37  __the_database = None 
  38  """The global 'Database' object.""" 
  39  __the_db_path = '.' 
  40  """The path to the database.""" 
  41   
  42  ######################################################################## 
  43  # Classes 
  44  ######################################################################## 
  45   
46 -class ItemDescriptor:
47 """An 'ItemDescriptor' describes a test, resource, or similar entity. 48 49 Some 'Database' operations return an instance of a class derived 50 from 'ItemDescriptor', rather than the object described. For 51 example, 'Database.GetTest' returns a 'TestDescriptor', not a 52 'Test'. This additional indirection is an optimization; the 53 creation of the actual 'Test' object may be relatively expensive, 54 and in many cases all that is needed is information that can be 55 gleaned from the descriptor.""" 56
57 - def __init__(self, 58 database, 59 instance_id, 60 class_name = None, 61 arguments = None, 62 item = None):
63 """Construct an 'ItemDescriptor'. 64 65 'database' -- The 'Database' object in which this entity is 66 located. 67 68 'instance_id' -- The label for this entity. 69 70 'class_name' -- The name of the extension class for the entity. 71 For example, for a 'TestDescriptor', the 'class_name' is the 72 name of the test class. Omit this argument if 'item' is provided. 73 74 'arguments' -- A dictionary mapping argument names to argument 75 values. These arguments will be provided to the extension class 76 when the entity is constructed. Omit this argument if 'item' is 77 provided. 78 79 'item' -- The item class for this item instance.""" 80 81 self.__database = database 82 self.__id = instance_id 83 assert(not item or not class_name) 84 self.__class_name = class_name 85 self.__arguments = arguments 86 self._item = item
87 88
89 - def GetDatabase(self):
90 """Return the 'Database' containing this entity. 91 92 returns -- The 'Database' object in which this entity is 93 located.""" 94 95 return self.__database
96 97
98 - def GetClassName(self):
99 """Return the class name of the entity. 100 101 returns -- The name of the extension class for the entity. For 102 example, for a 'TestDescriptor', this method returns the name of 103 the test class.""" 104 105 if self._item: 106 return self._item.GetClassName() 107 else: 108 return self.__class_name
109 110
111 - def GetClass(self):
112 """Return the class of the entity. 113 114 returns -- The Python class object for the entity. For example, 115 for a 'TestDescriptor', this method returns the test class.""" 116 117 raise NotImplementedError
118 119
120 - def GetClassArguments(self):
121 """Return the arguments specified by the test class. 122 123 returns -- A list of 'Field' objects containing all the 124 arguments in the class hierarchy. 125 126 Derived classes should not override this method.""" 127 128 return qm.extension.get_class_arguments(self.GetClass())
129 130
131 - def GetArguments(self):
132 """Return the entity arguments. 133 134 returns -- A dictionary mapping argument names to argument 135 values. These arguments will be provided to the extension class 136 when the entity is constructed.""" 137 138 if self._item: 139 return self._item.GetExplicitArguments() 140 else: 141 return self.__arguments
142 143
144 - def GetId(self):
145 """Return the label for this entity. 146 147 returns -- The label for this entity.""" 148 149 return self.__id
150 151
152 - def GetItem(self):
153 """Return the entity. 154 155 returns -- An instance of the class returned by 'GetClass'.""" 156 157 if not self._item: 158 extras = { Runnable.EXTRA_ID : self.GetId(), 159 Runnable.EXTRA_DATABASE : self.GetDatabase() } 160 self._item = self.GetClass()(self.GetArguments(), **extras) 161 162 return self._item
163 164
165 - def GetResources(self):
166 """Return the resources required by this item. 167 168 returns -- A sequence of resource names. Each name indicates a 169 resource that must be available to this item.""" 170 171 return self.GetArguments().get(Runnable.RESOURCE_FIELD_ID, [])
172 173 # Helper functions. 174
175 - def _Execute(self, context, result, method):
176 """Execute the entity. 177 178 'context' -- The 'Context' in which the test should be executed, 179 or 'None' if the 'method' does not take a 'Context' argument. 180 181 'result' -- The 'Result' object corresponding to this execution. 182 183 'method' -- The method name of the method on the entity that 184 should be invoked to perform the execution.""" 185 186 # Get the item. 187 item = self.GetItem() 188 methobj = getattr(item, method) 189 # Execute the indicated method. 190 if context is not None: 191 methobj(context, result) 192 else: 193 methobj(result)
194 195 196
197 -class TestDescriptor(ItemDescriptor):
198 """A test instance.""" 199
200 - def __init__(self, 201 database, 202 test_id, 203 test_class_name = None, 204 arguments = None, 205 test = None):
206 """Create a new test instance. 207 208 'database' -- The 'Database' containing this test. 209 210 'test_id' -- The test ID. 211 212 'test_class_name' -- The name of the test class of which this is 213 an instance. Omit this argument if 'test' is provided. 214 215 'arguments' -- This test's arguments to the test class. 216 Omit this argument if 'test' is provided. 217 218 'test' -- The test class of which this is an instance.""" 219 220 # Initialize the base class. 221 ItemDescriptor.__init__(self, database, 222 test_id, test_class_name, arguments, 223 test) 224 225 self.__prerequisites = {} 226 for p, o in \ 227 self.GetArguments().get(Test.PREREQUISITES_FIELD_ID, []): 228 self.__prerequisites[p] = o
229 230
231 - def GetClass(self):
232 """Return the class of the entity. 233 234 returns -- The Python class object for the entity. For example, 235 for a 'TestDescriptor', this method returns the test class.""" 236 237 if self._item: return type(self._item) 238 return get_extension_class(self.GetClassName(), 'test', 239 self.GetDatabase())
240 241
242 - def GetTest(self):
243 """Return the 'Test' object described by this descriptor.""" 244 245 return self.GetItem()
246 247
248 - def GetPrerequisites(self):
249 """Return a map from prerequisite test IDs to required outcomes.""" 250 251 return self.__prerequisites
252 253
254 - def GetTargetGroup(self):
255 """Returns the pattern for the targets that can run this test. 256 257 returns -- A regular expression (represented as a string) that 258 indicates the targets on which this test can be run. If the 259 pattern matches a particular group name, the test can be run 260 on targets in that group.""" 261 262 return self.GetArguments().get("target_group", ".*")
263 264
265 - def Run(self, context, result):
266 """Execute this test. 267 268 'context' -- The 'Context' in which the test should be executed. 269 270 'result' -- The 'Result' object for this test.""" 271 272 self._Execute(context, result, "Run")
273 274 275
276 -class ResourceDescriptor(ItemDescriptor):
277 """A resource instance.""" 278
279 - def __init__(self, 280 database, 281 resource_id, 282 resource_class_name = None, 283 arguments = None, 284 resource = None):
285 """Create a new resource instance. 286 287 'database' -- The 'Database' containing this resource. 288 289 'resource_id' -- The resource ID. 290 291 'resource_class_name' -- The name of the resource class of which 292 this is an instance. Omit this argument if 'resource' is provided. 293 294 'arguments' -- This resource's arguments to the resource class. 295 Omit this argument if 'resource' is provided. 296 297 'resource' -- The resource class of which this is an instance.""" 298 299 # Initialize the base class. 300 ItemDescriptor.__init__(self, database, resource_id, 301 resource_class_name, arguments, 302 resource)
303 304
305 - def GetClass(self):
306 """Return the class of the entity. 307 308 returns -- The Python class object for the entity. For example, 309 for a 'TestDescriptor', this method returns the test class.""" 310 311 if self._item: return type(self._item) 312 return get_extension_class(self.GetClassName(), 'resource', 313 self.GetDatabase())
314 315
316 - def GetResource(self):
317 """Return the 'Resource' object described by this descriptor.""" 318 319 return self.GetItem()
320 321
322 - def SetUp(self, context, result):
323 """Set up the resource. 324 325 'context' -- The 'Context' in which the resource should be executed. 326 327 'result' -- The 'Result' object for this resource.""" 328 329 self._Execute(context, result, "SetUp")
330 331
332 - def CleanUp(self, result):
333 """Clean up the resource. 334 335 'result' -- The 'Result' object for this resource.""" 336 337 self._Execute(None, result, "CleanUp")
338 339 340
341 -class DatabaseError(QMException):
342 """An exception relating to a 'Database'. 343 344 All exceptions raised directly by 'Database', or its derived 345 classes, will be instances of 'DatabaseError', or a class derived 346 from 'DatabaseError'. 347 348 If QMTest catches the exception, it will treat the string 349 representation of the exception as an error message to be formatted 350 for the user."""
351 352
353 -class NoSuchItemError(DatabaseError):
354 """An exception indicating that a particular item could not be found.""" 355
356 - def __init__(self, kind, item_id):
357 358 self.kind = kind 359 self.item_id = item_id
360 361
362 - def __str__(self):
363 """Return a string describing this exception.""" 364 365 return qm.message("no such item", 366 kind = self.kind, 367 item_id = self.item_id)
368 369 370
371 -class NoSuchTestError(NoSuchItemError):
372 """The specified test does not exist.""" 373
374 - def __init__(self, test_id):
375 """Construct a new 'NoSuchTestError' 376 377 'test_id' -- The name of the test that does not exist.""" 378 379 NoSuchItemError.__init__(self, Database.TEST, test_id)
380 381 382
383 -class NoSuchSuiteError(NoSuchItemError):
384 """The specified suite does not exist.""" 385
386 - def __init__(self, suite_id):
387 """Construct a new 'NoSuchSuiteError' 388 389 'suite_id' -- The name of the suite that does not exist.""" 390 391 NoSuchItemError.__init__(self, Database.SUITE, suite_id)
392 393 394
395 -class NoSuchResourceError(NoSuchItemError):
396 """The specified resource does not exist.""" 397
398 - def __init__(self, resource_id):
399 """Construct a new 'NoSuchResourceError' 400 401 'resource_id' -- The name of the resource that does not exist.""" 402 403 NoSuchItemError.__init__(self, Database.RESOURCE, resource_id)
404 405 406
407 -class Database(qm.extension.Extension):
408 """A 'Database' stores tests, testsuites, and resources. 409 410 A 'Database' has two primary functions: 411 412 1. Test storage and retrieval. 413 414 Every test has a unique name, called a "test id". When a new 415 test is created, the 'Database' is responsible for writing that 416 test to permanent storage. Later, QMTest will request the test 417 by providing the database with the test id. The database must 418 retrieve the test from permanent storage. 419 420 QMTest does not put any restrictions on *how* tests are stored. 421 The default database implementation uses XML to store tests, 422 but any storage format will do. 423 424 2. Test enumeration. 425 426 The 'Database' can tell QMTest what tests are stored in the 427 database. QMTest uses this information in its graphical user 428 interface to show the user what tests exist. 429 430 A 'Database' stores testsuites and resources in addition to tests. 431 The names for tests, testsuites, and resources are all "labels". A 432 label is a special kind of string that is designed to be easily 433 convertible to a file name. For more information, see the 434 'qm.label' module. The namespaces for tests, testsuites, and 435 resources are all distinct. For example, it is OK to have a test 436 with the same name as a testsuite. 437 438 Every 'Database' is associated with a particular directory on the 439 local machine. In most cases, the 'Database' will store all the 440 files it needs within this directory. 441 442 Every 'Database' has an associated 'AttachmentStore'. An 443 'AttachmentStore' is responsible for storing the attachments 444 associated with tests. See the module 'qm.attachment' for more 445 information about 'AttachmentStore'. 446 447 'Database' is an abstract class. 448 449 You can extend QMTest by providing your own database implementation. 450 One reason to do this is that you may want to store tests in a 451 format different from the XML format that QMTest uses by default. 452 For example, if you are testing a compiler, you might want to 453 represent each test as a source file. Or, if you are testing a 454 SQL database, you might want to represent each test as two files: 455 one containing SQL commands to run the test, and one containing 456 the output you expect. 457 458 Another reason to provide your own database implementation is that 459 you might want to store tests on a remote location. For example, 460 suppose you wanted to allow multiple users to access the same 461 central test database. You could create a test database that 462 created and retrieved tests by communicating with the central 463 server. 464 465 To create your own database implementation, you must create a Python 466 class derived (directly or indirectly) from 'Database'. The 467 documentation for each method of 'Database' indicates whether you 468 must override it in your database implementation. Some methods may 469 be overridden, but do not need to be. You might want to override 470 such a method to provide a more efficient implementation, but QMTest 471 will work fine if you just use the default version. 472 473 If QMTest calls a method on a database and that method raises an 474 exception that is not caught within the method itself, QMTest will 475 catch the exception and continue processing. Therefore, methods 476 here only have to handle exceptions themselves if that is necessary 477 to maintain the integrity of the database. 478 479 A single 'Database' may be accessed by multiple threads 480 simultaneously. Therefore, you must take appropriate steps to 481 ensure thread-safe access to shared data.""" 482 483 arguments = [ 484 qm.fields.TextField( 485 name = "label_class", 486 title = "Label Class", 487 description = """The name of the label class used by this database. 488 489 The label class is used to separate names of entities used 490 by the database into directories and basenames.""", 491 default_value = "python_label.PythonLabel" 492 ), 493 qm.fields.BooleanField( 494 name = "modifiable", 495 title = "Modifiable?", 496 description = """Whether or not the database can be modified. 497 498 If true, changes (such as the addition or removal of tests, 499 resources, or suites) can be made to the test database. 500 If false, tests can be viewed and run, but not modified.""", 501 default_value = "true") 502 ] 503 504 RESOURCE = "resource" 505 SUITE = "suite" 506 TEST = "test" 507 508 ITEM_KINDS = [RESOURCE, SUITE, TEST] 509 """The kinds of items that can be stored in a 'Database'.""" 510 511 _item_exceptions = { 512 RESOURCE : NoSuchResourceError, 513 SUITE : NoSuchSuiteError, 514 TEST : NoSuchTestError 515 } 516 """The exceptions to be raised when a particular item cannot be found. 517 518 This map is indexed by the 'ITEM_KINDS'; the value indicates the 519 exception class to be used when the indicated kind cannot be found.""" 520 521 _is_generic_database = False 522 """True if this database implements 'GetExtension' as a primitive. 523 524 Databases should implement 'GetExtension' and then override 525 '_is_generic_database', setting it to 'True'. However, legacy 526 databases implemented 'GetTest', 'GetResource', and 'GetSuite' as 527 primivites. These legacy databases should not override 528 '_generic_database'.""" 529 530 kind = "database" 531 """The 'Extension' kind.""" 532
533 - def __init__(self, path, arguments = None, **args):
534 """Construct a 'Database'. 535 536 'path' -- A string containing the absolute path to the directory 537 containing the database. 538 539 'arguments' -- A dictionary mapping attribute names to values. 540 The use of this parameter is deprecated. Use keyword arguments instead. 541 542 Derived classes must call this method from their own '__init__' 543 methods. Every derived class must have an '__init__' method that 544 takes the path to the directory containing the database as its 545 only argument. The path provided to the derived class '__init__' 546 function will always be an absolute path.""" 547 548 if arguments: args.update(arguments) 549 qm.extension.Extension.__init__(self, **args) 550 551 # The path given must be an absolute path. 552 assert os.path.isabs(path) 553 self.__path = path 554 555 # Translate the label class name into an actual Python class. 556 self.__label_class \ 557 = get_extension_class(self.label_class, "label", self)
558 559 # Methods that deal with labels. 560
561 - def IsValidLabel(self, label, is_component = 1):
562 """Return true if 'label' is valid. 563 564 'label' -- A string that is being considered as a label. 565 566 'is_component' -- True if the string being tested is just a 567 single component of a label path. 568 569 returns -- True if 'label' is a valid name for entities in this 570 database.""" 571 572 return self.__label_class("").IsValid(label, is_component)
573 574
575 - def JoinLabels(self, *labels):
576 """Join the 'labels' together. 577 578 'labels' -- A sequence of strings corresponding to label 579 components. 580 581 returns -- A string containing the complete label.""" 582 583 if not labels: 584 return "" 585 586 return str(apply(self.__label_class(labels[0]).Join, 587 labels[1:]))
588 589
590 - def SplitLabel(self, label):
591 """Split the label into a pair '(directory, basename)'. 592 593 returns -- A pair of strings '(directory, basename)'.""" 594 595 return map(str, self.__label_class(label).Split())
596 597
598 - def SplitLabelLeft(self, label):
599 """Split the label into a pair '(parent, subpath)'. 600 This is the same operation as SplitLabel, except the split 601 occurs at the leftmost separator, not the rightmost, and a 602 single-component label comes back in the parent slot. 603 604 returns -- A pair of strings '(parent, subpath)'.""" 605 606 return map(str, self.__label_class(label).SplitLeft())
607 608
609 - def GetLabelComponents(self, label):
610 """Return all of the component directories of 'label'. 611 612 'label' -- A string naming an entity in the database. 613 614 returns -- A list of strings. The first string is the first 615 directory in 'label'; the last string is the basename.""" 616 617 components = [] 618 while label: 619 dirname, label = self.SplitLabelLeft(label) 620 if dirname: 621 components.append(dirname) 622 else: 623 components.append(label) 624 break 625 626 return components
627 628 629 # Generic methods that deal with extensions. 630
631 - def GetExtension(self, id):
632 """Return the extension object named 'id'. 633 634 'id' -- The label for the extension. 635 636 returns -- The instance of 'Extension' with the indicated name, 637 or 'None' if there is no such entity. 638 639 Database classes should override this method. For backwards 640 compatibility, this base class implements this generic method 641 in terms of the special-purpose methods 'GetTest()' and 'GetResource()'. 642 Only if _is_generic_database is True are these implemented in terms 643 of 'GetExtension()'.""" 644 645 for kind in (Database.TEST, Database.RESOURCE): 646 try: 647 return self.GetItem(kind, id).GetItem() 648 except NoSuchItemError: 649 pass 650 651 try: 652 return self.GetSuite(id) 653 except NoSuchSuiteError: 654 pass 655 656 return None
657 658
659 - def GetExtensions(self, directory, scan_subdirs):
660 """Return the extensions in 'directory'. 661 662 'directory' -- The name of a directory. 663 664 'scan_subdirs' -- True if (and only if) subdirectories of 665 'directory' should be scanned. 666 667 returns -- A dictionary mapping labels to 'Extension' 668 instances. The dictionary contains all extensions in 669 'directory', and, if 'scan_subdirs' is true, its 670 subdirectories.""" 671 672 extensions = {} 673 674 for kind in self.ITEM_KINDS: 675 ids = self.GetIds(kind, directory, scan_subdirs) 676 for id in ids: 677 extensions[id] = self.GetExtension(id) 678 679 return extensions
680 681
682 - def RemoveExtension(self, id, kind):
683 """Remove the extension 'id' from the database. 684 685 'id' -- A label for the 'Extension' instance stored in the 686 database. 687 688 'kind' -- The kind of 'Extension' stored with the given 'id'.""" 689 690 raise NotImplementedError
691 692
693 - def WriteExtension(self, id, extension):
694 """Store 'extension' in the database, using the name 'id'. 695 696 'id' -- A label for the 'extension'. 697 698 'extension' -- An instance of 'Extension'. 699 700 The 'extension' is stored in the database. If there is a 701 previous item in the database with the same id, it is removed 702 and replaced with 'extension'. Some databases may not be able 703 to store all 'Extension' instances; those database must throw an 704 exception when an attempt is made to store such an 705 'extension'.""" 706 707 raise NotImplementedError
708 709 710 # Methods that deal with tests. 711
712 - def GetTest(self, test_id):
713 """Return the 'TestDescriptor' for the test named 'test_id'. 714 715 'test_id' -- A label naming the test. 716 717 returns -- A 'TestDescriptor' corresponding to 'test_id'. 718 719 raises -- 'NoSuchTestError' if there is no test in the database 720 named 'test_id'.""" 721 722 if self._is_generic_database: 723 test = self.GetExtension(test_id) 724 if isinstance(test, Test): 725 return TestDescriptor(self, test_id, test=test) 726 727 raise NoSuchTestError(test_id)
728 729
730 - def HasTest(self, test_id):
731 """Check whether or not the database has a test named 'test_id'. 732 733 'test_id' -- A label naming the test. 734 735 returns -- True if and only if the database contains a test 736 named 'test_id'. If this function returns true, 'GetTest' will 737 usually succeed. However, they may be circumstances where 738 'HasTest' returns true and 'GetTest' does not succeed. For 739 example, someone might remove a critical file from the database 740 between the time that 'HasTest' is called and the time that 741 'GetTest' is called. 742 743 Derived classes may override this method.""" 744 745 try: 746 self.GetTest(test_id) 747 except NoSuchTestError: 748 return 0 749 else: 750 return 1
751 752
753 - def GetTestIds(self, directory="", scan_subdirs=1):
754 """Return all test IDs that begin with 'directory'. 755 756 'directory' -- A label indicating the directory in which to 757 begin the search. 758 759 'scan_subdirs' -- True if (and only if) subdirectories of 760 'directory' should be scanned. 761 762 'returns' -- A list of all tests located within 'directory', 763 as absolute labels.""" 764 765 return self.GetIds(self.TEST, directory, scan_subdirs)
766 767 # Methods that deal with suites. 768
769 - def GetSuite(self, suite_id):
770 """Return the 'Suite' for the suite named 'suite_id'. 771 772 'suite_id' -- A label naming the suite. 773 774 returns -- An instance of 'Suite' (or a derived class of 775 'Suite') corresponding to 'suite_id'. 776 777 raises -- 'NoSuchSuiteError' if there is no test in the database 778 named 'test_id'. 779 780 All databases must have an implicit suite called '' that 781 contains all tests in the database. More generally, for each 782 directory in the database, there must be a corresponding suite 783 that contains all tests in that directory and its 784 subdirectories.""" 785 786 if suite_id == "": 787 return DirectorySuite(self, "") 788 789 if self._is_generic_database: 790 suite = self.GetExtension(suite_id) 791 if isinstance(suite, Suite): 792 return suite 793 794 raise NoSuchSuiteError(suite_id)
795 796
797 - def HasSuite(self, suite_id):
798 """Check whether or not the database has a suite named 'suite_id'. 799 800 'suite_id' -- A label naming the suite. 801 802 returns -- True if and only if the database contains a suite 803 named 'suite_id'. If this function returns true, 'GetSuite' 804 will usually succeed. However, they may be circumstances where 805 'HasSuite' returns true and 'GetSuite' does not succeed. For 806 example, someone might remove a critical file from the database 807 between the time that 'HasSuite' is called and the time that 808 'GetSuite' is called. 809 810 All databases must have an implicit suite called "" that 811 contains all tests in the database. More generally, for each 812 directory in the database, there must be a corresponding suite 813 that contains all tests in that directory and its 814 subdirectories. 815 816 Derived classes may override this method.""" 817 818 try: 819 self.GetSuite(suite_id) 820 except NoSuchSuiteError: 821 return 0 822 else: 823 return 1
824 825
826 - def GetSuiteIds(self, directory="", scan_subdirs=1):
827 """Return all suite IDs that begin with 'directory'. 828 829 'directory' -- A label indicating the directory in which to 830 begin the search. 831 832 'scan_subdirs' -- True if (and only if) subdirectories of 833 'directory' should be scanned. 834 835 'returns' -- A list of all suites located within 'directory', 836 as absolute labels.""" 837 838 return self.GetIds(self.SUITE, directory, scan_subdirs)
839 840 841 # Methods that deal with resources. 842
843 - def GetResource(self, resource_id):
844 """Return the 'ResourceDescriptor' for the resource 'resouce_id'. 845 846 'resource_id' -- A label naming the resource. 847 848 returns -- A 'ResourceDescriptor' corresponding to 'resource_id'. 849 850 raises -- 'NoSuchResourceError' if there is no resource in the 851 database named 'resource_id'.""" 852 853 if self._is_generic_database: 854 resource = self.GetExtension(resource_id) 855 if isinstance(resource, Resource): 856 return ResourceDescriptor(self, resource_id, resource = resource) 857 858 raise NoSuchResourceError(resource_id)
859 860
861 - def HasResource(self, resource_id):
862 """Check whether or not the database has a resource named 863 'resource_id'. 864 865 'resource_id' -- A label naming the resource. 866 867 returns -- True if and only if the database contains a resource 868 named 'resource_id'. If this function returns true, 869 'GetResource' will usually succeed. However, they may be 870 circumstances where 'HasResource' returns true and 'GetResource' 871 does not succeed. For example, someone might remove a critical 872 file from the database between the time that 'HasResource' is 873 called and the time that 'GetResource' is called. 874 875 Derived classes may override this method.""" 876 877 try: 878 self.GetResource(resource_id) 879 except NoSuchResourceError: 880 return 0 881 else: 882 return 1
883 884
885 - def GetResourceIds(self, directory="", scan_subdirs=1):
886 """Return all resource IDs that begin with 'directory'. 887 888 'directory' -- A label indicating the directory in which to 889 begin the search. 890 891 'scan_subdirs' -- True if (and only if) subdirectories of 892 'directory' should be scanned. 893 894 'returns' -- A list of all resources located within 'directory', 895 as absolute labels.""" 896 897 return self.GetIds(self.RESOURCE, directory, scan_subdirs)
898 899 # Miscellaneous methods. 900
901 - def GetIds(self, kind, directory = "", scan_subdirs = 1):
902 """Return all IDs of the indicated 'kind' that begin with 'directory'. 903 904 'kind' -- One of the 'ITEM_KINDS'. 905 906 'directory' -- A label indicating the directory in which to 907 begin the search. 908 909 'scan_subdirs' -- True if (and only if) subdirectories of 910 'directory' should be scanned. 911 912 returns -- A list of all items of the indicated 'kind' located 913 within 'directory', as absolute labels. 914 915 Derived classes may override this method.""" 916 917 if self._is_generic_database: 918 extensions = self.GetExtensions(directory, scan_subdirs) 919 extensions = filter(lambda e: e.kind == kind, 920 extensions.values()) 921 return map(lambda e: e.GetId(), extensions) 922 923 return []
924 925
926 - def GetItem(self, kind, item_id):
927 """Return the item of the indicated 'kind' with indicated 'item_id'. 928 929 'kind' -- One of the 'ITEM_KINDS'. 930 931 'item_id' -- The name of the item. 932 933 returns -- If 'kind' is 'Database.TEST' or 'Database.RESOURCE', 934 returns a test descriptor or resource descriptor, respectively. 935 If 'kind' is 'Database.SUITE', returns a 'Suite'. 936 937 Derived classes may override this method.""" 938 939 return { Database.TEST : self.GetTest, 940 Database.RESOURCE : self.GetResource, 941 Database.SUITE : self.GetSuite } [kind] (item_id)
942 943 944
945 - def GetSubdirectories(self, directory):
946 """Return the immediate subdirectories of 'directory'. 947 948 'directory' -- A label indicating a directory in the database. 949 950 returns -- A sequence of (relative) labels indictating the 951 immediate subdirectories of 'directory'. For example, if "a.b" 952 and "a.c" are directories in the database, this method will 953 return "b" and "c" given "a" as 'directory'. 954 955 Derived classes may override this method.""" 956 957 return []
958 959
960 - def GetPath(self):
961 """Return the directory containing the database. 962 963 returns -- A string containing the absolute path to the 964 directory containing the database. 965 966 Derived classes must not override this method.""" 967 968 return self.__path
969 970
971 - def GetConfigurationDirectory(self):
972 """Return the directory containing configuration information. 973 974 returns -- The directory containing configuration information 975 for the database. 976 977 Derived classes must not override this method.""" 978 979 return get_configuration_directory(self.GetPath())
980 981
982 - def GetAttachmentStore(self):
983 """Returns the 'AttachmentStore' associated with the database. 984 985 returns -- The 'AttachmentStore' containing the attachments 986 associated with tests and resources in this database. 987 988 Derived classes may override this method.""" 989 990 return None
991 992
993 - def GetClassPaths(self):
994 """Return directories to search for test and resource classes. 995 996 returns -- A sequence of strings. Each string is a directory 997 that should be searched to locate test and resource classes. 998 The directories will be searched in the order they appear. 999 QMTest will search other directories (like those in the 1000 'QMTEST_CLASS_PATH' environment variable) in addition to these 1001 directories. 1002 1003 For a given database, this method should always return the same 1004 value; callers are permitted to cache the value returned. 1005 1006 Derived classes may override this method. The sequence returned 1007 by the derived class need not be a superset of the value 1008 returned by the default implementation (but probably should 1009 be).""" 1010 1011 return []
1012 1013
1014 - def GetTestClassNames(self):
1015 """Return the kinds of tests that the database can store. 1016 1017 returns -- A sequence of strings. Each string names a 1018 class, including the containing module. Only classes 1019 of these types can be stored in the database. 1020 1021 Derived classes may override this method. The default 1022 implementation allows all available test classes, but the 1023 derived class may allow only a subset.""" 1024 1025 return get_extension_class_names('test', self)
1026 1027
1028 - def GetResourceClassNames(self):
1029 """Return the kinds of resources that the database can store. 1030 1031 returns -- A sequence of strings. Each string names a 1032 class, including the containing module. Only resources 1033 of these types can be stored in the database. 1034 1035 Derived classes may override this method. The default 1036 implementation allows all available resource classes, but the 1037 derived class may allow only a subset.""" 1038 1039 return get_extension_class_names('resource', self)
1040 1041
1042 - def ExpandIds(self, ids):
1043 """Expand test and suite IDs into test IDs. 1044 1045 'ids' -- A sequence of IDs of tests and suites, which may be mixed 1046 together. 1047 1048 returns -- A pair 'test_ids, suite_ids'. 'test_ids' is a 1049 sequence of test IDs including all test IDs mentioned in 'ids' plus 1050 all test IDs obtained from recursively expanding suites included in 1051 'ids'. 'suite_ids' is the set of IDs of suites included directly 1052 and indirectly in 'ids'. 1053 1054 raises -- 'ValueError' if an element in 'id' is neither a test or 1055 suite ID. The exception argument is the erroneous element.""" 1056 1057 # We'll collect test and suite IDs in maps, to make duplicate 1058 # checks efficient. 1059 test_ids = {} 1060 suite_ids = {} 1061 1062 for id in ids: 1063 # Skip this ID if we've already seen it. 1064 if suite_ids.has_key(id) or test_ids.has_key(id): 1065 continue 1066 # Is this a suite ID? 1067 if self.HasSuite(id): 1068 suite_ids[id] = None 1069 # Yes. Load the suite. 1070 suite = self.GetSuite(id) 1071 # Determine all the tests and suites contained directly and 1072 # indirectly in this suite. 1073 suite_test_ids, sub_suite_ids = suite.GetAllTestAndSuiteIds() 1074 # Add them. 1075 for test_id in suite_test_ids: 1076 test_ids[test_id] = None 1077 for suite_id in sub_suite_ids: 1078 suite_ids[suite_id] = None 1079 # Or is this a test ID? 1080 elif self.HasTest(id): 1081 # Yes. Add it. 1082 test_ids[id] = None 1083 else: 1084 # It doesn't look like a test or suite ID. 1085 raise ValueError, id 1086 1087 # Convert the maps to sequences. 1088 return test_ids.keys(), suite_ids.keys()
1089 1090
1091 - def IsModifiable(self):
1092 """Returns true iff this database is modifiable. 1093 1094 returns -- True iff this database is modifiable. If the 1095 database is modifiable, it supports operatings like 'Write' 1096 that make changes to the structure of the databaes itself. 1097 Otherwise, the contents of the database may be viewed, but not 1098 modified.""" 1099 1100 return self.modifiable == "true"
1101 1102 ######################################################################## 1103 # Functions 1104 ######################################################################## 1105
1106 -def get_configuration_directory(path):
1107 """Return the configuration directory for the 'Database' rooted at 'path'. 1108 1109 'path' -- The path to the test database. 1110 1111 returns -- The path to the configuration directory.""" 1112 1113 return os.path.join(path, "QMTest")
1114 1115
1116 -def get_configuration_file(path):
1117 """Return the configuration file for the 'Database' rooted at 'path'. 1118 1119 'path' -- The path to the test database. 1120 1121 returns -- The path to the configuration file.""" 1122 1123 return os.path.join(get_configuration_directory(path), 1124 "configuration")
1125 1126
1127 -def is_database(db_path):
1128 """Returns true if 'db_path' looks like a test database.""" 1129 1130 # A test database is a directory. 1131 if not os.path.isdir(db_path): 1132 return 0 1133 # A test database contains a configuration file. 1134 if not os.path.isfile(get_configuration_file(db_path)): 1135 return 0 1136 # It probably is OK. 1137 return 1
1138 1139
1140 -def load_database(db_path):
1141 """Load the database from 'db_path'. 1142 1143 'db_path' -- The path to the directory containing the database. 1144 1145 returns -- The new 'Database'.""" 1146 1147 # Make sure it is a directory. 1148 if not is_database(db_path): 1149 raise QMException, \ 1150 qm.error("not test database", path=db_path) 1151 1152 # Load the file. 1153 config_path = get_configuration_file(db_path) 1154 document = qm.xmlutil.load_xml_file(config_path) 1155 1156 # Parse it. 1157 database_class, arguments \ 1158 = (qm.extension.parse_dom_element 1159 (document.documentElement, 1160 lambda n: qm.test.base.get_extension_class(n, "database", 1161 None, db_path))) 1162 # For backwards compatibility with QM 1.1.x, we look for "attribute" 1163 # elements. 1164 for node in document.documentElement.getElementsByTagName("attribute"): 1165 name = node.getAttribute("name") 1166 # These elements were only allowed to contain strings as 1167 # values. 1168 value = qm.xmlutil.get_dom_text(node) 1169 # Python does not allow keyword arguments to have Unicode 1170 # values, so we convert the name to an ordinary string. 1171 arguments[str(name)] = value 1172 1173 return database_class(db_path, arguments)
1174 1175 1176 ######################################################################## 1177 # Functions 1178 ######################################################################## 1179
1180 -def set_path(path):
1181 """Set the database path to be used when the database is loaded. 1182 1183 'path' -- A string containing the path to the database.""" 1184 1185 global __the_db_path 1186 1187 __the_db_path = path
1188 1189
1190 -def get_database():
1191 """Returns the global Database object. 1192 1193 returns -- The 'Database' object that corresponds to the currently 1194 executing process. It may be None.""" 1195 1196 global __the_database 1197 1198 if not __the_database: 1199 __the_database = load_database(__the_db_path) 1200 1201 return __the_database
1202 1203 1204 ######################################################################## 1205 # Local Variables: 1206 # mode: python 1207 # indent-tabs-mode: nil 1208 # fill-column: 72 1209 # End: 1210