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

Source Code for Module qm.test.cmdline

   1  ######################################################################## 
   2  # 
   3  # File:   cmdline.py 
   4  # Author: Alex Samuel 
   5  # Date:   2001-03-16 
   6  # 
   7  # Contents: 
   8  #   QMTest command processing 
   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 base 
  21  import database 
  22  import os 
  23  import qm 
  24  import qm.attachment 
  25  import qm.cmdline 
  26  import qm.platform 
  27  from   qm.extension import get_extension_class_name, get_class_description 
  28  from   qm.test import test 
  29  from   qm.test.result import Result 
  30  from   qm.test.context import * 
  31  from   qm.test.execution_engine import * 
  32  from   qm.test.result_stream import ResultStream 
  33  from   qm.test.runnable import Runnable 
  34  from   qm.test.suite import Suite 
  35  from   qm.test.report import ReportGenerator 
  36  from   qm.test.classes.dir_run_database import * 
  37  from   qm.test.expectation_database import ExpectationDatabase 
  38  from   qm.test.classes.previous_testrun import PreviousTestRun 
  39  from   qm.trace import * 
  40  from   qm.test.web.web import QMTestServer 
  41  import qm.structured_text 
  42  import qm.xmlutil 
  43  import Queue 
  44  import random 
  45  from   result import * 
  46  import signal 
  47  import string 
  48  import sys 
  49  import xml.sax 
  50   
  51  ######################################################################## 
  52  # Variables 
  53  ######################################################################## 
  54   
  55  _the_qmtest = None 
  56  """The global 'QMTest' object.""" 
  57   
  58  ######################################################################## 
  59  # Functions 
  60  ######################################################################## 
  61   
62 -def _make_comma_separated_string (items, conjunction):
63 """Return a string consisting of the 'items', separated by commas. 64 65 'items' -- A list of strings giving the items in the list. 66 67 'conjunction' -- A string to use before the final item, if there is 68 more than one. 69 70 returns -- A string consisting all of the 'items', separated by 71 commas, and with the 'conjunction' before the final item.""" 72 73 s = "" 74 need_comma = 0 75 # Go through almost all of the items, adding them to the 76 # comma-separated list. 77 for i in items[:-1]: 78 # Add a comma if this isn't the first item in the list. 79 if need_comma: 80 s += ", " 81 else: 82 need_comma = 1 83 # Add this item. 84 s += "'%s'" % i 85 # The last item is special, because we need to include the "or". 86 if items: 87 i = items[-1] 88 if need_comma: 89 s += ", %s " % conjunction 90 s += "'%s'" % i 91 92 return s
93 94 ######################################################################## 95 # Classes 96 ######################################################################## 97
98 -class QMTest:
99 """An instance of QMTest.""" 100 101 __extension_kinds_string \ 102 = _make_comma_separated_string(base.extension_kinds, "or") 103 """A string listing the available extension kinds.""" 104 105 db_path_environment_variable = "QMTEST_DB_PATH" 106 """The environment variable specifying the test database path.""" 107 108 summary_formats = ("brief", "full", "stats", "batch", "none") 109 """Valid formats for result summaries.""" 110 111 context_file_name = "context" 112 """The default name of a context file.""" 113 114 expectations_file_name = "expectations.qmr" 115 """The default name of a file containing expectations.""" 116 117 results_file_name = "results.qmr" 118 """The default name of a file containing results.""" 119 120 target_file_name = "targets" 121 """The default name of a file containing targets.""" 122 123 help_option_spec = ( 124 "h", 125 "help", 126 None, 127 "Display usage summary." 128 ) 129 130 version_option_spec = ( 131 None, 132 "version", 133 None, 134 "Display version information." 135 ) 136 137 db_path_option_spec = ( 138 "D", 139 "tdb", 140 "PATH", 141 "Path to the test database." 142 ) 143 144 extension_output_option_spec = ( 145 "o", 146 "output", 147 "FILE", 148 "Write the extension to FILE.", 149 ) 150 151 extension_id_option_spec = ( 152 "i", 153 "id", 154 "NAME", 155 "Write the extension to the database as NAME.", 156 ) 157 158 output_option_spec = ( 159 "o", 160 "output", 161 "FILE", 162 "Write test results to FILE (- for stdout)." 163 ) 164 165 no_output_option_spec = ( 166 None, 167 "no-output", 168 None, 169 "Don't generate test results." 170 ) 171 172 outcomes_option_spec = ( 173 "O", 174 "outcomes", 175 "FILE", 176 "Use expected outcomes in FILE." 177 ) 178 179 expectations_option_spec = ( 180 "e", 181 "expectations", 182 "FILE", 183 "Use expectations in FILE." 184 ) 185 186 context_option_spec = ( 187 "c", 188 "context", 189 "KEY=VALUE", 190 "Add or override a context property." 191 ) 192 193 context_file_spec = ( 194 "C", 195 "load-context", 196 "FILE", 197 "Read context from a file (- for stdin)." 198 ) 199 200 daemon_option_spec = ( 201 None, 202 "daemon", 203 None, 204 "Run as a daemon." 205 ) 206 207 port_option_spec = ( 208 "P", 209 "port", 210 "PORT", 211 "Server port number." 212 ) 213 214 address_option_spec = ( 215 "A", 216 "address", 217 "ADDRESS", 218 "Local address." 219 ) 220 221 log_file_option_spec = ( 222 None, 223 "log-file", 224 "PATH", 225 "Log file name." 226 ) 227 228 no_browser_option_spec = ( 229 None, 230 "no-browser", 231 None, 232 "Do not open a new browser window." 233 ) 234 235 pid_file_option_spec = ( 236 None, 237 "pid-file", 238 "PATH", 239 "Process ID file name." 240 ) 241 242 concurrent_option_spec = ( 243 "j", 244 "concurrency", 245 "COUNT", 246 "Execute tests in COUNT concurrent threads." 247 ) 248 249 targets_option_spec = ( 250 "T", 251 "targets", 252 "FILE", 253 "Use FILE as the target specification file." 254 ) 255 256 random_option_spec = ( 257 None, 258 "random", 259 None, 260 "Run the tests in a random order." 261 ) 262 263 rerun_option_spec = ( 264 None, 265 "rerun", 266 "FILE", 267 "Rerun the tests that failed." 268 ) 269 270 seed_option_spec = ( 271 None, 272 "seed", 273 "INTEGER", 274 "Seed the random number generator." 275 ) 276 277 format_option_spec = ( 278 "f", 279 "format", 280 "FORMAT", 281 "Specify the summary format." 282 ) 283 284 result_stream_spec = ( 285 None, 286 "result-stream", 287 "CLASS-NAME", 288 "Specify the results file format." 289 ) 290 291 annotation_option_spec = ( 292 "a", 293 "annotate", 294 "NAME=VALUE", 295 "Set an additional annotation to be written to the result stream(s)." 296 ) 297 298 tdb_class_option_spec = ( 299 "c", 300 "class", 301 "CLASS-NAME", 302 "Specify the test database class.", 303 ) 304 305 attribute_option_spec = ( 306 "a", 307 "attribute", 308 "NAME", 309 "Get an attribute of the extension class." 310 ) 311 312 set_attribute_option_spec = ( 313 "a", 314 "attribute", 315 "KEY=VALUE", 316 "Set an attribute of the extension class." 317 ) 318 319 extension_kind_option_spec = ( 320 "k", 321 "kind", 322 "EXTENSION-KIND", 323 "Specify the kind of extension class." 324 ) 325 326 report_output_option_spec = ( 327 "o", 328 "output", 329 "FILE", 330 "Write test report to FILE (- for stdout)." 331 ) 332 333 report_flat_option_spec = ( 334 "f", 335 "flat", 336 None, 337 """Generate a flat listing of test results, instead of reproducing the 338 database directory tree in the report.""" 339 ) 340 341 results_option_spec = ( 342 "R", 343 "results", 344 "DIRECTORY", 345 "Read in all results (*.qmr) files from DIRECTORY." 346 ) 347 348 list_long_option_spec = ( 349 "l", 350 "long", 351 None, 352 "Use a detailed output format." 353 ) 354 355 list_details_option_spec = ( 356 "d", 357 "details", 358 None, 359 "Display details for individual items." 360 ) 361 362 list_recursive_option_spec = ( 363 "R", 364 "recursive", 365 None, 366 "Recursively list the contents of directories." 367 ) 368 369 # Groups of options that should not be used together. 370 conflicting_option_specs = ( 371 ( output_option_spec, no_output_option_spec ), 372 ( concurrent_option_spec, targets_option_spec ), 373 ( extension_output_option_spec, extension_id_option_spec ), 374 ( expectations_option_spec, outcomes_option_spec ), 375 ) 376 377 global_options_spec = [ 378 help_option_spec, 379 version_option_spec, 380 db_path_option_spec, 381 ] 382 383 commands_spec = [ 384 ("create", 385 "Create (or update) an extension.", 386 "EXTENSION-KIND CLASS-NAME(ATTR1 = 'VAL1', ATTR2 = 'VAL2', ...)", 387 """Create (or update) an extension. 388 389 The EXTENSION-KIND indicates what kind of extension to 390 create; it must be one of """ + __extension_kinds_string + """. 391 392 The CLASS-NAME indicates the name of the extension class, or 393 the name of an existing extension object. If the CLASS-NAME 394 is the name of a extension in the test database, then the 395 396 In the former case, it must have the form 'MODULE.CLASS'. For 397 a list of available extension classes use "qmtest extensions". 398 If the extension class takes arguments, those arguments can be 399 specified after the CLASS-NAME as show above. In the latter 400 case, 401 402 Any "--attribute" options are processed before the arguments 403 specified after the class name. Therefore, the "--attribute" 404 options can be overridden by the arguments provided after the 405 CLASS-NAME. If no attributes are specified, the parentheses 406 following the 'CLASS-NAME' can be omitted. 407 408 If the "--id" option is given, the extension is written to the 409 database. Otherwise, if the "--output" option is given, the 410 extension is written as XML to the file indicated. If neither 411 option is given, the extension is written as XML to the 412 standard output.""", 413 ( set_attribute_option_spec, 414 help_option_spec, 415 extension_id_option_spec, 416 extension_output_option_spec 417 ), 418 ), 419 420 ("create-target", 421 "Create (or update) a target specification.", 422 "NAME CLASS [ GROUP ]", 423 "Create (or update) a target specification.", 424 ( set_attribute_option_spec, 425 help_option_spec, 426 targets_option_spec 427 ) 428 ), 429 430 ("create-tdb", 431 "Create a new test database.", 432 "", 433 "Create a new test database.", 434 ( help_option_spec, 435 tdb_class_option_spec, 436 set_attribute_option_spec) 437 ), 438 439 ("gui", 440 "Start the QMTest GUI.", 441 "", 442 "Start the QMTest graphical user interface.", 443 ( 444 address_option_spec, 445 concurrent_option_spec, 446 context_file_spec, 447 context_option_spec, 448 daemon_option_spec, 449 help_option_spec, 450 log_file_option_spec, 451 no_browser_option_spec, 452 pid_file_option_spec, 453 port_option_spec, 454 outcomes_option_spec, 455 targets_option_spec, 456 results_option_spec 457 ) 458 ), 459 460 ("extensions", 461 "List extension classes.", 462 "", 463 """ 464 List the available extension classes. 465 466 Use the '--kind' option to limit the classes displayed to test classes, 467 resource classes, etc. The parameter to '--kind' can be one of """ + \ 468 __extension_kinds_string + "\n", 469 ( 470 extension_kind_option_spec, 471 help_option_spec, 472 ) 473 ), 474 475 ("describe", 476 "Describe an extension.", 477 "EXTENSION-KIND NAME", 478 """Display details for the specified extension.""", 479 ( 480 attribute_option_spec, 481 list_long_option_spec, 482 help_option_spec, 483 ) 484 ), 485 486 ("help", 487 "Display usage summary.", 488 "", 489 "Display usage summary.", 490 () 491 ), 492 493 ("ls", 494 "List database contents.", 495 "[ NAME ... ]", 496 """ 497 List items stored in the database. 498 499 If no arguments are provided, the contents of the root 500 directory of the database are displayed. Otherwise, each of 501 the database is searched for each of the NAMEs. If the item 502 found is a directory then the contents of the directory are 503 displayed. 504 """, 505 ( 506 help_option_spec, 507 list_long_option_spec, 508 list_details_option_spec, 509 list_recursive_option_spec, 510 ), 511 ), 512 513 ("register", 514 "Register an extension class.", 515 "KIND CLASS", 516 """ 517 Register an extension class with QMTest. KIND is the kind of extension 518 class to register; it must be one of """ + __extension_kinds_string + """ 519 520 The CLASS gives the name of the class in the form 'module.class'. 521 522 QMTest will search the available extension class directories to find the 523 new CLASS. QMTest looks for files whose basename is the module name and 524 whose extension is either '.py', '.pyc', or '.pyo'. 525 526 QMTest will then attempt to load the extension class. If the extension 527 class cannot be loaded, QMTest will issue an error message to help you 528 debug the problem. Otherwise, QMTest will update the 'classes.qmc' file 529 in the directory containing the module to mention your new extension class. 530 """, 531 (help_option_spec,) 532 ), 533 534 ("remote", 535 "Run QMTest as a remote server.", 536 "", 537 """ 538 Runs QMTest as a remote server. This mode is only used by QMTest 539 itself when distributing tests across multiple machines. Users 540 should not directly invoke QMTest with this option. 541 """, 542 (help_option_spec,) 543 ), 544 545 ("report", 546 "Generate report from one or more test results.", 547 "[ result [-e expected] ]+", 548 """ 549 Generates a test report. The arguments are result files each optionally 550 followed by '-e' and an expectation file. This command attempts to reproduce 551 the test database structure, and thus requires the '--tdb' option. To generate 552 a flat test report specify the '--flat' option. 553 """, 554 (help_option_spec, 555 report_output_option_spec, 556 report_flat_option_spec) 557 ), 558 559 ("run", 560 "Run one or more tests.", 561 "[ ID ... ]", 562 """ 563 Runs tests. Optionally, generates a summary of the test run and a 564 record of complete test results. You may specify test IDs and test 565 suite IDs to run; omit arguments to run the entire test database. 566 567 Test results are written to "results.qmr". Use the '--output' option to 568 specify a different output file, or '--no-output' to supress results. 569 570 Use the '--format' option to specify the output format for the summary. 571 Valid formats are %s. 572 """ % _make_comma_separated_string(summary_formats, "and"), 573 ( 574 annotation_option_spec, 575 concurrent_option_spec, 576 context_file_spec, 577 context_option_spec, 578 format_option_spec, 579 help_option_spec, 580 no_output_option_spec, 581 outcomes_option_spec, 582 expectations_option_spec, 583 output_option_spec, 584 random_option_spec, 585 rerun_option_spec, 586 result_stream_spec, 587 seed_option_spec, 588 targets_option_spec, 589 ) 590 ), 591 592 ("summarize", 593 "Summarize results from a test run.", 594 "[FILE [ ID ... ]]", 595 """ 596 Loads a test results file and summarizes the results. FILE is the path 597 to the results file. Optionally, specify one or more test or suite IDs 598 whose results are shown. If none are specified, shows all tests that 599 did not pass. 600 601 Use the '--format' option to specify the output format for the summary. 602 Valid formats are %s. 603 """ % _make_comma_separated_string(summary_formats, "and"), 604 ( help_option_spec, 605 format_option_spec, 606 outcomes_option_spec, 607 expectations_option_spec, 608 output_option_spec, 609 result_stream_spec) 610 ), 611 612 ] 613 614 __version_output = \ 615 ("QMTest %s\n" 616 "Copyright (C) 2002 - 2007 CodeSourcery, Inc.\n" 617 "QMTest comes with ABSOLUTELY NO WARRANTY\n" 618 "For more information about QMTest visit http://www.qmtest.com\n") 619 """The string printed when the --version option is used. 620 621 There is one fill-in, for a string, which should contain the version 622 number.""" 623
624 - def __init__(self, argument_list, path):
625 """Construct a new QMTest. 626 627 Parses the argument list but does not execute the command. 628 629 'argument_list' -- The arguments to QMTest, not including the 630 initial argv[0]. 631 632 'path' -- The path to the QMTest executable.""" 633 634 global _the_qmtest 635 636 _the_qmtest = self 637 638 # Use the stadard stdout and stderr streams to emit messages. 639 self._stdout = sys.stdout 640 self._stderr = sys.stderr 641 642 # Build a trace object. 643 self.__tracer = Tracer() 644 645 # Build a command-line parser for this program. 646 self.__parser = qm.cmdline.CommandParser( 647 "qmtest", 648 self.global_options_spec, 649 self.commands_spec, 650 self.conflicting_option_specs) 651 # Parse the command line. 652 components = self.__parser.ParseCommandLine(argument_list) 653 # Unpack the results. 654 ( self.__global_options, 655 self.__command, 656 self.__command_options, 657 self.__arguments 658 ) = components 659 660 # If available, record the path to the qmtest executable. 661 self.__qmtest_path = path 662 663 # We have not yet computed the set of available targets. 664 self.targets = None 665 666 # The result stream class used for results files is the pickling 667 # version. 668 self.__file_result_stream_class_name \ 669 = "pickle_result_stream.PickleResultStream" 670 # The result stream class used for textual feed back. 671 self.__text_result_stream_class_name \ 672 = "text_result_stream.TextResultStream" 673 # The expected outcomes have not yet been loaded. 674 self.__expected_outcomes = None
675 676
677 - def __del__(self):
678 """Clean up global variables.""" 679 680 test.set_targets([])
681 682
683 - def HasGlobalOption(self, option):
684 """Return true if 'option' was specified as a global command. 685 686 'command' -- The long name of the option, but without the 687 preceding "--". 688 689 returns -- True if the option is present.""" 690 691 return option in map(lambda x: x[0], self.__global_options)
692 693
694 - def GetGlobalOption(self, option, default=None):
695 """Return the value of global 'option', or 'default' if omitted.""" 696 697 for opt, opt_arg in self.__global_options: 698 if opt == option: 699 return opt_arg 700 return default
701 702
703 - def HasCommandOption(self, option):
704 """Return true if command 'option' was specified.""" 705 706 for opt, opt_arg in self.__command_options: 707 if opt == option: 708 return 1 709 return 0
710 711
712 - def GetCommandOption(self, option, default = None):
713 """Return the value of command 'option'. 714 715 'option' -- The long form of an command-specific option. 716 717 'default' -- The default value to be returned if the 'option' 718 was not specified. This option should be the kind of an option 719 that takes an argument. 720 721 returns -- The value specified by the option, or 'default' if 722 the option was not specified.""" 723 724 for opt, opt_arg in self.__command_options: 725 if opt == option: 726 return opt_arg 727 return default
728 729
730 - def Execute(self):
731 """Execute the command. 732 733 returns -- 0 if the command was executed successfully. 1 if 734 there was a problem or if any tests run had unexpected outcomes.""" 735 736 # If --version was given, print the version number and exit. 737 # (The GNU coding standards require that the program take no 738 # further action after seeing --version.) 739 if self.HasGlobalOption("version"): 740 self._stdout.write(self.__version_output % qm.version) 741 return 0 742 # If the global help option was specified, display it and stop. 743 if (self.GetGlobalOption("help") is not None 744 or self.__command == "help"): 745 self._stdout.write(self.__parser.GetBasicHelp()) 746 return 0 747 # If the command help option was specified, display it and stop. 748 if self.GetCommandOption("help") is not None: 749 self.__WriteCommandHelp(self.__command) 750 return 0 751 752 # Make sure a command was specified. 753 if self.__command == "": 754 raise qm.cmdline.CommandError, qm.error("missing command") 755 756 # Look in several places to find the test database: 757 # 758 # 1. The command-line. 759 # 2. The QMTEST_DB_PATH environment variable. 760 # 3. The current directory. 761 db_path = self.GetGlobalOption("tdb") 762 if not db_path: 763 if os.environ.has_key(self.db_path_environment_variable): 764 db_path = os.environ[self.db_path_environment_variable] 765 else: 766 db_path = "." 767 # If the path is not already absolute, make it into an 768 # absolute path at this point. 769 if not os.path.isabs(db_path): 770 db_path = os.path.join(os.getcwd(), db_path) 771 # Normalize the path so that it is easy for the user to read 772 # if it is emitted in an error message. 773 self.__db_path = os.path.normpath(db_path) 774 database.set_path(self.__db_path) 775 776 error_occurred = 0 777 778 # Dispatch to the appropriate method. 779 if self.__command == "create-tdb": 780 return self.__ExecuteCreateTdb(db_path) 781 782 method = { 783 "create" : self.__ExecuteCreate, 784 "create-target" : self.__ExecuteCreateTarget, 785 "describe" : self.__ExecuteDescribe, 786 "extensions" : self.__ExecuteExtensions, 787 "gui" : self.__ExecuteServer, 788 "ls" : self.__ExecuteList, 789 "register" : self.__ExecuteRegister, 790 "remote" : self.__ExecuteRemote, 791 "run" : self.__ExecuteRun, 792 "report" : self.__ExecuteReport, 793 "summarize": self.__ExecuteSummarize, 794 }[self.__command] 795 796 return method()
797 798
799 - def GetDatabase(self):
800 """Return the test database to use. 801 802 returns -- The 'Database' to use for this execution. Raises an 803 exception if no 'Database' is available.""" 804 805 return database.get_database()
806 807
808 - def GetDatabaseIfAvailable(self):
809 """Return the test database to use. 810 811 returns -- The 'Database' to use for this execution, or 'None' 812 if no 'Database' is available.""" 813 814 try: 815 return self.GetDatabase() 816 except: 817 return None
818 819
820 - def GetTargetFileName(self):
821 """Return the path to the file containing target specifications. 822 823 returns -- The path to the file containing target specifications.""" 824 825 # See if the user requested a specific target file. 826 target_file_name = self.GetCommandOption("targets") 827 if target_file_name: 828 return target_file_name 829 # If there was no explicit option, use the "targets" file in the 830 # database directory. 831 return os.path.join(self.GetDatabase().GetConfigurationDirectory(), 832 self.target_file_name)
833 834
835 - def GetTargetsFromFile(self, file_name):
836 """Return the 'Target's specified in 'file_name'. 837 838 returns -- A list of the 'Target' objects specified in the 839 target specification file 'file_name'.""" 840 841 try: 842 document = qm.xmlutil.load_xml_file(file_name) 843 targets_element = document.documentElement 844 if targets_element.tagName != "targets": 845 raise QMException, \ 846 qm.error("could not load target file", 847 file = file_name) 848 targets = [] 849 for node in targets_element.getElementsByTagName("extension"): 850 # Parse the DOM node. 851 target_class, arguments \ 852 = (qm.extension.parse_dom_element 853 (node, 854 lambda n: get_extension_class(n, "target", 855 self.GetDatabase()))) 856 # Build the target. 857 target = target_class(self.GetDatabase(), arguments) 858 # Accumulate targets. 859 targets.append(target) 860 861 return targets 862 except Context: 863 raise QMException, \ 864 qm.error("could not load target file", 865 file=file_name)
866 867 868
869 - def GetTargets(self):
870 """Return the 'Target' objects specified by the user. 871 872 returns -- A sequence of 'Target' objects.""" 873 874 if not test.get_targets(): 875 file_name = self.GetTargetFileName() 876 if os.path.exists(file_name): 877 test.set_targets(self.GetTargetsFromFile(file_name)) 878 else: 879 # The target file does not exist. 880 concurrency = self.GetCommandOption("concurrency") 881 if concurrency is None: 882 # No concurrency specified. Run single-threaded. 883 concurrency = 1 884 else: 885 # Convert the concurrency to an integer. 886 try: 887 concurrency = int(concurrency) 888 except ValueError: 889 raise qm.cmdline.CommandError, \ 890 qm.error("concurrency not integer", 891 value=concurrency) 892 # Construct the target. 893 arguments = {} 894 arguments["name"] = "local" 895 arguments["group"] = "local" 896 if concurrency > 1: 897 class_name = "thread_target.ThreadTarget" 898 arguments["threads"] = concurrency 899 else: 900 class_name = "serial_target.SerialTarget" 901 target_class = get_extension_class(class_name, 902 'target', self.GetDatabase()) 903 test.set_targets([target_class(self.GetDatabase(), arguments)]) 904 905 return test.get_targets()
906 907
908 - def GetTracer(self):
909 """Return the 'Tracer' associated with this instance of QMTest. 910 911 returns -- The 'Tracer' associated with this instance of QMTest.""" 912 913 return self.__tracer
914 915
916 - def MakeContext(self):
917 """Construct a 'Context' object for running tests.""" 918 919 context = Context() 920 921 # First, see if a context file was specified on the command 922 # line. 923 use_implicit_context_file = 1 924 for option, argument in self.__command_options: 925 if option == "load-context": 926 use_implicit_context_file = 0 927 break 928 929 # If there is no context file, read the default context file. 930 if (use_implicit_context_file 931 and os.path.isfile(self.context_file_name)): 932 context.Read(self.context_file_name) 933 934 for option, argument in self.__command_options: 935 # Look for the '--load-context' option. 936 if option == "load-context": 937 context.Read(argument) 938 # Look for the '--context' option. 939 elif option == "context": 940 # Parse the argument. 941 name, value = qm.common.parse_assignment(argument) 942 943 try: 944 # Insert it into the context. 945 context[name] = value 946 except ValueError, msg: 947 # The format of the context key is invalid, but 948 # raise a 'CommandError' instead. 949 raise qm.cmdline.CommandError, msg 950 951 return context
952 953
954 - def GetExecutablePath(self):
955 """Return the path to the QMTest executable. 956 957 returns -- A string giving the path to the QMTest executable. 958 This is the path that should be used to invoke QMTest 959 recursively. Returns 'None' if the path to the QMTest 960 executable is uknown.""" 961 962 return self.__qmtest_path
963 964
965 - def GetFileResultStreamClass(self):
966 """Return the 'ResultStream' class used for results files. 967 968 returns -- The 'ResultStream' class used for results files.""" 969 970 return get_extension_class(self.__file_result_stream_class_name, 971 "result_stream", 972 self.GetDatabaseIfAvailable())
973
974 - def GetTextResultStreamClass(self):
975 """Return the 'ResultStream' class used for textual feedback. 976 977 returns -- the 'ResultStream' class used for textual 978 feedback.""" 979 980 return get_extension_class(self.__text_result_stream_class_name, 981 "result_stream", 982 self.GetDatabaseIfAvailable())
983 984
985 - def __GetAttributeOptions(self, expect_value = True):
986 """Return the attributes specified on the command line. 987 988 'expect_value' -- True if the attribute is to be parsed as 989 an assignment. 990 991 returns -- A dictionary. If expect_value is True, it 992 maps attribute names (strings) to values (strings). 993 Else it contains the raw attribute strings, mapping to None. 994 There is an entry for each attribute specified with 995 '--attribute' on the command line.""" 996 997 # There are no attributes yet. 998 attributes = {} 999 1000 # Go through the command line looking for attribute options. 1001 for option, argument in self.__command_options: 1002 if option == "attribute": 1003 if expect_value: 1004 name, value = qm.common.parse_assignment(argument) 1005 attributes[name] = value 1006 else: 1007 attributes[argument] = None 1008 return attributes
1009 1010
1011 - def __GetAnnotateOptions(self):
1012 """Return all annotate options. 1013 1014 returns -- A dictionary containing the annotation name / value pairs.""" 1015 1016 annotations = {} 1017 for option, argument in self.__command_options: 1018 if option == "annotate": 1019 name, value = qm.common.parse_assignment(argument) 1020 annotations[name] = value 1021 return annotations
1022 1023
1024 - def __ExecuteCreate(self):
1025 """Create a new extension file.""" 1026 1027 # Check that the right number of arguments are present. 1028 if len(self.__arguments) != 2: 1029 self.__WriteCommandHelp("create") 1030 return 2 1031 1032 # Figure out what database (if any) we are using. 1033 database = self.GetDatabaseIfAvailable() 1034 1035 # Get the extension kind. 1036 kind = self.__arguments[0] 1037 self.__CheckExtensionKind(kind) 1038 1039 extension_id = self.GetCommandOption("id") 1040 if extension_id is not None: 1041 if not database: 1042 raise QMException, qm.error("no db specified") 1043 if not database.IsModifiable(): 1044 raise QMException, qm.error("db not modifiable") 1045 extension_loader = database.GetExtension 1046 else: 1047 extension_loader = None 1048 1049 class_loader = lambda n: get_extension_class(n, kind, database) 1050 1051 # Process the descriptor. 1052 (extension_class, more_arguments) \ 1053 = (qm.extension.parse_descriptor 1054 (self.__arguments[1], class_loader, extension_loader)) 1055 1056 # Validate the --attribute options. 1057 arguments = self.__GetAttributeOptions() 1058 arguments = qm.extension.validate_arguments(extension_class, 1059 arguments) 1060 # Override the --attribute options with the arguments provided 1061 # as part of the descriptor. 1062 arguments.update(more_arguments) 1063 1064 if extension_id is not None: 1065 # Create the extension instance. Objects derived from 1066 # Runnable require magic additional arguments. 1067 if issubclass(extension_class, (Runnable, Suite)): 1068 extras = { extension_class.EXTRA_ID : extension_id, 1069 extension_class.EXTRA_DATABASE : database } 1070 else: 1071 extras = {} 1072 extension = extension_class(arguments, **extras) 1073 # Write the extension to the database. 1074 database.WriteExtension(extension_id, extension) 1075 else: 1076 # Figure out what file to use. 1077 filename = self.GetCommandOption("output") 1078 if filename is not None: 1079 file = open(filename, "w") 1080 else: 1081 file = sys.stdout 1082 # Write out the file. 1083 qm.extension.write_extension_file(extension_class, arguments, 1084 file) 1085 1086 return 0
1087 1088
1089 - def __ExecuteCreateTdb(self, db_path):
1090 """Handle the command for creating a new test database. 1091 1092 'db_path' -- The path at which to create the new test database.""" 1093 1094 if len(self.__arguments) != 0: 1095 self.__WriteCommandHelp("create-tdb") 1096 return 2 1097 1098 # Create the directory if it does not already exists. 1099 if not os.path.isdir(db_path): 1100 os.mkdir(db_path) 1101 # Create the configuration directory. 1102 config_dir = database.get_configuration_directory(db_path) 1103 if not os.path.isdir(config_dir): 1104 os.mkdir(config_dir) 1105 1106 # Reformulate this command in terms of "qmtest create". Start by 1107 # adding "--output <path>". 1108 self.__command_options.append(("output", 1109 database.get_configuration_file(db_path))) 1110 # Figure out what database class to use. 1111 class_name \ 1112 = self.GetCommandOption("class", "xml_database.XMLDatabase") 1113 # Add the extension kind and descriptor. 1114 self.__arguments.append("database") 1115 self.__arguments.append(class_name) 1116 # Now process this just like "qmtest create". 1117 self.__ExecuteCreate() 1118 # Print a helpful message. 1119 self._stdout.write(qm.message("new db message", path=db_path) + "\n") 1120 1121 return 0
1122 1123
1124 - def __ExecuteCreateTarget(self):
1125 """Create a new target file.""" 1126 1127 # Make sure that the arguments are correct. 1128 if (len(self.__arguments) < 2 or len(self.__arguments) > 3): 1129 self.__WriteCommandHelp("create-target") 1130 return 2 1131 1132 # Pull the required arguments out of the command line. 1133 target_name = self.__arguments[0] 1134 class_name = self.__arguments[1] 1135 if (len(self.__arguments) > 2): 1136 target_group = self.__arguments[2] 1137 else: 1138 target_group = "" 1139 1140 # Load the database. 1141 database = self.GetDatabase() 1142 1143 # Load the target class. 1144 target_class = get_extension_class(class_name, "target", database) 1145 1146 # Get the dictionary of class arguments. 1147 field_dictionary \ 1148 = qm.extension.get_class_arguments_as_dictionary(target_class) 1149 1150 # Get the name of the target file. 1151 file_name = self.GetTargetFileName() 1152 # If the file already exists, read it in. 1153 if os.path.exists(file_name): 1154 # Load the document. 1155 document = qm.xmlutil.load_xml_file(file_name) 1156 # If there is a previous entry for this target, discard it. 1157 targets_element = document.documentElement 1158 duplicates = [] 1159 for target_element \ 1160 in targets_element.getElementsByTagName("extension"): 1161 for attribute \ 1162 in target_element.getElementsByTagName("argument"): 1163 if attribute.getAttribute("name") == "name": 1164 name = field_dictionary["name"].\ 1165 GetValueFromDomNode(attribute.childNodes[0], 1166 None) 1167 if name == target_name: 1168 duplicates.append(target_element) 1169 break 1170 for duplicate in duplicates: 1171 targets_element.removeChild(duplicate) 1172 duplicate.unlink() 1173 else: 1174 document = (qm.xmlutil.create_dom_document 1175 (public_id = "QMTest/Target", 1176 document_element_tag = "targets")) 1177 targets_element = document.documentElement 1178 1179 # Get the attributes. 1180 attributes = self.__GetAttributeOptions() 1181 attributes["name"] = target_name 1182 attributes["group"] = target_group 1183 attributes = qm.extension.validate_arguments(target_class, 1184 attributes) 1185 1186 # Create the target element. 1187 target_element = qm.extension.make_dom_element(target_class, 1188 attributes, 1189 document) 1190 targets_element.appendChild(target_element) 1191 1192 # Write out the XML file. 1193 document.writexml(open(self.GetTargetFileName(), "w")) 1194 1195 return 0
1196 1197
1198 - def __ExecuteExtensions(self):
1199 """List the available extension classes.""" 1200 1201 # Check that the right number of arguments are present. 1202 if len(self.__arguments) != 0: 1203 self.__WriteCommandHelp("extensions") 1204 return 2 1205 1206 database = self.GetDatabaseIfAvailable() 1207 1208 # Figure out what kinds of extensions we're going to list. 1209 kind = self.GetCommandOption("kind") 1210 if kind: 1211 self.__CheckExtensionKind(kind) 1212 kinds = [kind] 1213 else: 1214 kinds = base.extension_kinds 1215 1216 for kind in kinds: 1217 # Get the available classes. 1218 names = qm.test.base.get_extension_class_names(kind, 1219 database, 1220 self.__db_path) 1221 # Build structured text describing the classes. 1222 description = "** Available %s classes **\n\n" % kind 1223 for n in names: 1224 description += " * " + n + "\n\n " 1225 # Try to load the class to get more information. 1226 try: 1227 extension_class = get_extension_class(n, kind, database) 1228 description \ 1229 += qm.extension.get_class_description(extension_class, 1230 brief=1) 1231 except: 1232 description += ("No description available: " 1233 "could not load class.") 1234 description += "\n\n" 1235 1236 self._stdout.write(qm.structured_text.to_text(description)) 1237 1238 return 0
1239 1240
1241 - def __ExecuteDescribe(self):
1242 """Describe an extension.""" 1243 1244 # Check that the right number of arguments are present. 1245 if len(self.__arguments) != 2: 1246 self.__WriteCommandHelp("describe") 1247 return 2 1248 1249 kind = self.__arguments[0] 1250 long_format = self.GetCommandOption("long") != None 1251 1252 database = self.GetDatabaseIfAvailable() 1253 class_ = get_extension_class(self.__arguments[1], kind, database) 1254 1255 attributes = (self.__GetAttributeOptions(False) 1256 or class_._argument_dictionary) 1257 1258 print "" 1259 print "class name:", get_extension_class_name(class_) 1260 print " ", get_class_description(class_, brief=not long_format) 1261 print "" 1262 print "class attributes:" 1263 tab = max([len(name) for name in attributes]) 1264 for name in attributes: 1265 field = class_._argument_dictionary.get(name) 1266 if not field: 1267 self._stderr.write("Unknown attribute '%s'.\n"%name) 1268 return 2 1269 value = field.GetDefaultValue() 1270 description = field.GetDescription() 1271 if not long_format: 1272 description = qm.structured_text.get_first(description) 1273 print " %-*s %s"%(tab, name, description)
1274 1275
1276 - def __ExecuteList(self):
1277 """List the contents of the database.""" 1278 1279 database = self.GetDatabase() 1280 1281 long_format = self.HasCommandOption("long") 1282 details_format = self.HasCommandOption("details") 1283 recursive = self.HasCommandOption("recursive") 1284 1285 # If no arguments are specified, list the root directory. 1286 args = self.__arguments or ("",) 1287 1288 # Get all the extensions to list. 1289 extensions = {} 1290 for arg in args: 1291 extension = database.GetExtension(arg) 1292 if not extension: 1293 raise QMException, qm.error("no such ID", id = arg) 1294 if isinstance(extension, qm.test.suite.Suite): 1295 if recursive: 1296 test_ids, suite_ids = extension.GetAllTestAndSuiteIds() 1297 extensions.update([(i, database.GetExtension(i)) 1298 for i in test_ids + suite_ids]) 1299 else: 1300 ids = extension.GetTestIds() + extension.GetSuiteIds() 1301 extensions.update([(i, database.GetExtension(i)) 1302 for i in ids]) 1303 else: 1304 extensions[arg] = extension 1305 1306 # Get the labels for the extensions, in alphabetical order. 1307 ids = extensions.keys() 1308 ids.sort() 1309 1310 # In the short format, just print the labels. 1311 if not long_format: 1312 for id in ids: 1313 print >> sys.stdout, id 1314 return 0 1315 1316 # In the long format, print three columns: the extension kind, 1317 # class name, and the label. We make two passes over the 1318 # extensions so that the output will be tidy. In the first pass, 1319 # calculate the width required for the first two columns in the 1320 # output. The actual output occurs in the second pass. 1321 longest_kind = 0 1322 longest_class = 0 1323 for i in (0, 1): 1324 for id in ids: 1325 extension = extensions[id] 1326 if isinstance(extension, 1327 qm.test.directory_suite.DirectorySuite): 1328 kind = "directory" 1329 class_name = "" 1330 else: 1331 kind = extension.__class__.kind 1332 class_name = extension.GetClassName() 1333 1334 if i == 0: 1335 kind_len = len(kind) + 1 1336 if kind_len > longest_kind: 1337 longest_kind = kind_len 1338 class_len = len(class_name) + 1 1339 if class_len > longest_class: 1340 longest_class = class_len 1341 else: 1342 print >> sys.stdout, \ 1343 "%-*s%-*s%s" % (longest_kind, kind, 1344 longest_class, class_name, id) 1345 if details_format: 1346 tab = max([len(name) 1347 for name in extension._argument_dictionary]) 1348 for name in extension._argument_dictionary: 1349 value = str(getattr(extension, name)) 1350 print " %-*s %s"%(tab, name, value) 1351 1352 return 0
1353 1354
1355 - def __ExecuteRegister(self):
1356 """Register a new extension class.""" 1357 1358 # Make sure that the KIND and CLASS were specified. 1359 if (len(self.__arguments) != 2): 1360 self.__WriteCommandHelp("register") 1361 return 2 1362 kind = self.__arguments[0] 1363 class_name = self.__arguments[1] 1364 1365 # Check that the KIND is valid. 1366 self.__CheckExtensionKind(kind) 1367 1368 # Check that the CLASS_NAME is well-formed. 1369 if class_name.count('.') != 1: 1370 raise qm.cmdline.CommandError, \ 1371 qm.error("invalid class name", 1372 class_name = class_name) 1373 module, name = class_name.split('.') 1374 1375 # Try to load the database. It may provide additional 1376 # directories to search. 1377 database = self.GetDatabaseIfAvailable() 1378 # Hunt through all of the extension class directories looking 1379 # for an appropriately named module. 1380 found = None 1381 directories = get_extension_directories(kind, database, 1382 self.__db_path) 1383 for directory in directories: 1384 for ext in (".py", ".pyc", ".pyo"): 1385 file_name = os.path.join(directory, module + ext) 1386 if os.path.exists(file_name): 1387 found = file_name 1388 break 1389 if found: 1390 break 1391 1392 # If we could not find the module, issue an error message. 1393 if not found: 1394 raise qm.QMException, \ 1395 qm.error("module does not exist", 1396 module = module) 1397 1398 # Inform the user of the location in which QMTest found the 1399 # module. (Sometimes, there might be another module with the 1400 # same name in the path. Telling the user where we've found 1401 # the module will help the user to deal with this situation.) 1402 self._stdout.write(qm.structured_text.to_text 1403 (qm.message("loading class", 1404 class_name = name, 1405 file_name = found))) 1406 1407 # We have found the module. Try loading it. 1408 extension_class = get_extension_class_from_directory(class_name, 1409 kind, 1410 directory, 1411 directories) 1412 1413 # Create or update the classes.qmc file. 1414 classes_file_name = os.path.join(directory, "classes.qmc") 1415 1416 # Create a new DOM document for the class directory. 1417 document = (qm.xmlutil.create_dom_document 1418 (public_id = "Class-Directory", 1419 document_element_tag="class-directory")) 1420 1421 # Copy entries from the old file to the new one. 1422 extensions = get_extension_class_names_in_directory(directory) 1423 for k, ns in extensions.iteritems(): 1424 for n in ns: 1425 # Remove previous entries for the class being added. 1426 if k == kind and n == class_name: 1427 continue 1428 element = document.createElement("class") 1429 element.setAttribute("kind", k) 1430 element.setAttribute("name", n) 1431 document.documentElement.appendChild(element) 1432 1433 # Add an entry for the new element. 1434 element = document.createElement("class") 1435 element.setAttribute("kind", kind) 1436 element.setAttribute("name", class_name) 1437 document.documentElement.appendChild(element) 1438 1439 # Write out the file. 1440 document.writexml(open(classes_file_name, "w"), 1441 addindent = " ", newl = "\n") 1442 1443 return 0
1444 1445
1446 - def __ExecuteSummarize(self):
1447 """Read in test run results and summarize.""" 1448 1449 # If no results file is specified, use a default value. 1450 if len(self.__arguments) == 0: 1451 results_path = self.results_file_name 1452 else: 1453 results_path = self.__arguments[0] 1454 1455 database = self.GetDatabaseIfAvailable() 1456 1457 # The remaining arguments, if any, are test and suite IDs. 1458 id_arguments = self.__arguments[1:] 1459 # Are there any? 1460 # '.' is an alias for <all>, and thus shadows other selectors. 1461 if len(id_arguments) > 0 and not '.' in id_arguments: 1462 ids = set() 1463 # Expand arguments into test/resource IDs. 1464 if database: 1465 for id in id_arguments: 1466 extension = database.GetExtension(id) 1467 if not extension: 1468 raise qm.cmdline.CommandError, \ 1469 qm.error("no such ID", id = id) 1470 if extension.kind == database.SUITE: 1471 ids.update(extension.GetAllTestAndSuiteIds()[0]) 1472 else: 1473 ids.add(id) 1474 else: 1475 ids = set(id_arguments) 1476 else: 1477 # No IDs specified. Show all test and resource results. 1478 # Don't show any results by test suite though. 1479 ids = None 1480 1481 # Get an iterator over the results. 1482 try: 1483 results = base.load_results(results_path, database) 1484 except Exception, exception: 1485 raise QMException, \ 1486 qm.error("invalid results file", 1487 path=results_path, 1488 problem=str(exception)) 1489 1490 any_unexpected_outcomes = 0 1491 1492 # Load expectations. 1493 expectations = (self.GetCommandOption('expectations') or 1494 self.GetCommandOption('outcomes')) 1495 expectations = base.load_expectations(expectations, 1496 database, 1497 results.GetAnnotations()) 1498 # Compute the list of result streams to which output should be 1499 # written. 1500 streams = self.__CreateResultStreams(self.GetCommandOption("output"), 1501 results.GetAnnotations(), 1502 expectations) 1503 1504 resource_results = {} 1505 for r in results: 1506 if r.GetKind() != Result.TEST: 1507 if ids is None or r.GetId() in ids: 1508 for s in streams: 1509 s.WriteResult(r) 1510 elif r.GetKind() == Result.RESOURCE_SETUP: 1511 resource_results[r.GetId()] = r 1512 continue 1513 # We now known that r is test result. If it's not one 1514 # that interests us, we're done. 1515 if ids is not None and r.GetId() not in ids: 1516 continue 1517 # If we're filtering, and this test was not run because it 1518 # depended on a resource that was not set up, emit the 1519 # resource result here. 1520 if (ids is not None 1521 and r.GetOutcome() == Result.UNTESTED 1522 and r.has_key(Result.RESOURCE)): 1523 rid = r[Result.RESOURCE] 1524 rres = resource_results.get(rid) 1525 if rres: 1526 del resource_results[rid] 1527 for s in streams: 1528 s.WriteResult(rres) 1529 # Write out the test result. 1530 for s in streams: 1531 s.WriteResult(r) 1532 if (not any_unexpected_outcomes 1533 and r.GetOutcome() != expectations.Lookup(r.GetId())): 1534 any_unexpected_outcomes = 1 1535 # Shut down the streams. 1536 for s in streams: 1537 s.Summarize() 1538 1539 return any_unexpected_outcomes
1540 1541
1542 - def __ExecuteRemote(self):
1543 """Execute the 'remote' command.""" 1544 1545 database = self.GetDatabase() 1546 1547 # Get the target class. For now, we always run in serial when 1548 # running remotely. 1549 target_class = get_extension_class("serial_target.SerialTarget", 1550 'target', database) 1551 # Build the target. 1552 target = target_class(database, { "name" : "child" }) 1553 1554 # Start the target. 1555 response_queue = Queue.Queue(0) 1556 target.Start(response_queue) 1557 1558 # Read commands from standard input, and reply to standard 1559 # output. 1560 while 1: 1561 # Read the command. 1562 command = cPickle.load(sys.stdin) 1563 1564 # If the command is just a string, it should be 1565 # the 'Stop' command. 1566 if isinstance(command, types.StringType): 1567 assert command == "Stop" 1568 target.Stop() 1569 break 1570 1571 # Decompose command. 1572 method, id, context = command 1573 # Get the descriptor. 1574 descriptor = database.GetTest(id) 1575 # Run it. 1576 target.RunTest(descriptor, context) 1577 # There are no results yet. 1578 results = [] 1579 # Read all of the results. 1580 while 1: 1581 try: 1582 result = response_queue.get(0) 1583 results.append(result) 1584 except Queue.Empty: 1585 # There are no more results. 1586 break 1587 # Pass the results back. 1588 cPickle.dump(results, sys.stdout) 1589 # The standard output stream is bufferred, but the master 1590 # will block waiting for a response, so we must flush 1591 # the buffer here. 1592 sys.stdout.flush() 1593 1594 return 0
1595 1596
1597 - def __ExecuteReport(self):
1598 """Execute a 'report' command.""" 1599 1600 output = self.GetCommandOption("output") 1601 flat = self.GetCommandOption("flat") != None 1602 1603 # Check that at least one result file is present. 1604 if not output or len(self.__arguments) < 1: 1605 self.__WriteCommandHelp("report") 1606 return 2 1607 1608 # If the database can be loaded, use it to find all 1609 # available tests. The default (non-flat) format requires a database. 1610 if flat: 1611 database = self.GetDatabaseIfAvailable() 1612 else: 1613 database = self.GetDatabase() 1614 1615 report_generator = ReportGenerator(output, database) 1616 report_generator.GenerateReport(flat, self.__arguments)
1617 1618
1619 - def __ExecuteRun(self):
1620 """Execute a 'run' command.""" 1621 1622 database = self.GetDatabase() 1623 1624 # Handle the 'seed' option. First create the random number 1625 # generator we will use. 1626 seed = self.GetCommandOption("seed") 1627 if seed: 1628 # A seed was specified. It should be an integer. 1629 try: 1630 seed = int(seed) 1631 except ValueError: 1632 raise qm.cmdline.CommandError, \ 1633 qm.error("seed not integer", seed=seed) 1634 # Use the specified seed. 1635 random.seed(seed) 1636 1637 # Figure out what tests to run. 1638 if len(self.__arguments) == 0: 1639 # No IDs specified; run the entire test database. 1640 self.__arguments.append("") 1641 elif '.' in self.__arguments: 1642 # '.' is an alias for <all>, and thus shadows other selectors. 1643 self.__arguments = [""] 1644 1645 # Expand arguments in test IDs. 1646 try: 1647 test_ids, test_suites \ 1648 = self.GetDatabase().ExpandIds(self.__arguments) 1649 except (qm.test.database.NoSuchTestError, 1650 qm.test.database.NoSuchSuiteError), exception: 1651 raise qm.cmdline.CommandError, str(exception) 1652 except ValueError, exception: 1653 raise qm.cmdline.CommandError, \ 1654 qm.error("no such ID", id=str(exception)) 1655 1656 # Handle the --annotate options. 1657 annotations = self.__GetAnnotateOptions() 1658 1659 # Load expectations. 1660 expectations = (self.GetCommandOption('expectations') or 1661 self.GetCommandOption('outcomes')) 1662 expectations = base.load_expectations(expectations, 1663 database, 1664 annotations) 1665 # Filter the set of tests to be run, eliminating any that should 1666 # be skipped. 1667 test_ids = self.__FilterTestsToRun(test_ids, expectations) 1668 1669 # Figure out which targets to use. 1670 targets = self.GetTargets() 1671 # Compute the context in which the tests will be run. 1672 context = self.MakeContext() 1673 1674 # Handle the --output option. 1675 if self.HasCommandOption("no-output"): 1676 # User specified no output. 1677 result_file_name = None 1678 else: 1679 result_file_name = self.GetCommandOption("output") 1680 if result_file_name is None: 1681 # By default, write results to a default file. 1682 result_file_name = self.results_file_name 1683 1684 # Compute the list of result streams to which output should be 1685 # written. 1686 result_streams = self.__CreateResultStreams(result_file_name, 1687 annotations, 1688 expectations) 1689 1690 if self.HasCommandOption("random"): 1691 # Randomize the order of the tests. 1692 random.shuffle(test_ids) 1693 else: 1694 test_ids.sort() 1695 1696 # Run the tests. 1697 engine = ExecutionEngine(database, test_ids, context, targets, 1698 result_streams, 1699 expectations) 1700 if engine.Run(): 1701 return 1 1702 1703 return 0
1704 1705
1706 - def __ExecuteServer(self):
1707 """Process the server command.""" 1708 1709 database = self.GetDatabase() 1710 1711 # Get the port number specified by a command option, if any. 1712 # Otherwise use a default value. 1713 port_number = self.GetCommandOption("port", default=0) 1714 try: 1715 port_number = int(port_number) 1716 except ValueError: 1717 raise qm.cmdline.CommandError, qm.error("bad port number") 1718 # Get the local address specified by a command option, if any. 1719 # If not was specified, use the loopback address. The loopback 1720 # address is used by default for security reasons; it restricts 1721 # access to the QMTest server to users on the local machine. 1722 address = self.GetCommandOption("address", default="127.0.0.1") 1723 1724 # If a log file was requested, open it now. 1725 log_file_path = self.GetCommandOption("log-file") 1726 if log_file_path == "-": 1727 # A hyphen path name means standard output. 1728 log_file = sys.stdout 1729 elif log_file_path is None: 1730 # No log file. 1731 log_file = None 1732 else: 1733 # Otherwise, it's a file name. Open it for append. 1734 log_file = open(log_file_path, "a+") 1735 1736 # If a PID file was requested, create it now. 1737 pid_file_path = self.GetCommandOption("pid-file") 1738 if pid_file_path is not None: 1739 # If a PID file was requested, but no explicit path was 1740 # given, use a default value. 1741 if not pid_file_path: 1742 pid_file_path = qm.common.rc.Get("pid-file", 1743 "/var/run/qmtest.pid", 1744 "qmtest") 1745 try: 1746 pid_file = open(pid_file_path, "w") 1747 except IOError, e: 1748 raise qm.cmdline.CommandError, str(e) 1749 else: 1750 pid_file = None 1751 1752 # Create a run database, if requested. 1753 run_db = None 1754 directory = self.GetCommandOption("results", default="") 1755 if directory: 1756 directory = os.path.normpath(directory) 1757 run_db = DirRunDatabase(directory, database) 1758 1759 # Load expectations. Only support the 'outcome' option here, 1760 # as 'expectations' in general are unsupported with this GUI. 1761 expectations = self.GetCommandOption('outcomes') 1762 expectations = base.load_expectations(expectations, database) 1763 # Make sure this is either an ExpectationDatabase or a 1764 # PreviousRun 1765 if not type(expectations) in (ExpectationDatabase, PreviousTestRun): 1766 raise qm.cmdline.CommandError, 'not a valid results file' 1767 # Figure out which targets to use. 1768 targets = self.GetTargets() 1769 # Compute the context in which the tests will be run. 1770 context = self.MakeContext() 1771 # Set up the server. 1772 server = QMTestServer(database, 1773 port_number, address, 1774 log_file, targets, context, 1775 expectations, 1776 run_db) 1777 port_number = server.GetServerAddress()[1] 1778 1779 # Construct the URL to the main page on the server. 1780 if address == "": 1781 url_address = qm.platform.get_host_name() 1782 else: 1783 url_address = address 1784 if run_db: 1785 url = "http://%s:%d/report/dir" % (url_address, port_number) 1786 else: 1787 url = "http://%s:%d/test/dir" % (url_address, port_number) 1788 1789 if not self.HasCommandOption("no-browser"): 1790 # Now that the server is bound to its address, start the 1791 # web browser. 1792 qm.platform.open_in_browser(url) 1793 1794 message = qm.message("server url", url=url) 1795 sys.stderr.write(message + "\n") 1796 1797 # Become a daemon, if appropriate. 1798 if self.GetCommandOption("daemon") is not None: 1799 # Fork twice. 1800 if os.fork() != 0: 1801 os._exit(0) 1802 if os.fork() != 0: 1803 os._exit(0) 1804 # This process is now the grandchild of the original 1805 # process. 1806 1807 # Write out the PID file. The correct PID is not known until 1808 # after the transformation to a daemon has taken place. 1809 try: 1810 if pid_file: 1811 pid_file.write(str(os.getpid())) 1812 pid_file.close() 1813 1814 # Accept requests. 1815 try: 1816 server.Run() 1817 except qm.platform.SignalException, se: 1818 if se.GetSignalNumber() == signal.SIGTERM: 1819 # If we receive SIGTERM, shut down. 1820 pass 1821 else: 1822 # Other signals propagate outwards. 1823 raise 1824 except KeyboardInterrupt: 1825 # If we receive a keyboard interrupt (Ctrl-C), shut down. 1826 pass 1827 finally: 1828 if pid_file: 1829 os.remove(pid_file_path) 1830 1831 return 0
1832 1833
1834 - def __WriteCommandHelp(self, command):
1835 """Write out help information about 'command'. 1836 1837 'command' -- The name of the command for which help information 1838 is required.""" 1839 1840 self._stderr.write(self.__parser.GetCommandHelp(command))
1841
1842 - def __FilterTestsToRun(self, test_ids, expectations):
1843 """Return those tests from 'test_ids' that should be run. 1844 1845 'test_ids' -- A sequence of test ids. 1846 1847 'expectations' -- An ExpectationDatabase. 1848 1849 returns -- Those elements of 'test_names' that are not to be 1850 skipped. If 'a' precedes 'b' in 'test_ids', and both 'a' and 1851 'b' are present in the result, 'a' will precede 'b' in the 1852 result.""" 1853 1854 # The --rerun option indicates that only failing tests should 1855 # be rerun. 1856 rerun_file_name = self.GetCommandOption("rerun") 1857 if rerun_file_name: 1858 # Load the outcomes from the file specified. 1859 outcomes = base.load_outcomes(rerun_file_name, 1860 self.GetDatabase()) 1861 # Filter out tests that have unexpected outcomes. 1862 test_ids = [t for t in test_ids 1863 if outcomes.get(t, Result.PASS) 1864 != expectations.Lookup(t).GetOutcome()] 1865 1866 return test_ids
1867 1868
1869 - def __CheckExtensionKind(self, kind):
1870 """Check that 'kind' is a valid extension kind. 1871 1872 'kind' -- A string giving the name of an extension kind. If the 1873 'kind' does not name a valid extension kind, an appropriate 1874 exception is raised.""" 1875 1876 if kind not in base.extension_kinds: 1877 raise qm.cmdline.CommandError, \ 1878 qm.error("invalid extension kind", 1879 kind = kind)
1880 1881
1882 - def __CreateResultStreams(self, output_file, annotations, expectations):
1883 """Return the result streams to use. 1884 1885 'output_file' -- If not 'None', the name of a file to which 1886 the standard results file format should be written. 1887 1888 'annotations' -- A dictionary with annotations for this test run. 1889 1890 'expectations' -- An ExpectationDatabase. 1891 1892 returns -- A list of 'ResultStream' objects, as indicated by the 1893 user.""" 1894 1895 database = self.GetDatabaseIfAvailable() 1896 1897 result_streams = [] 1898 1899 arguments = {} 1900 arguments['expected_outcomes'] = expectations.GetExpectedOutcomes() 1901 1902 # Look up the summary format. 1903 format = self.GetCommandOption("format", "") 1904 if format and format not in self.summary_formats: 1905 # Invalid format. Complain. 1906 valid_format_string = string.join( 1907 map(lambda f: '"%s"' % f, self.summary_formats), ", ") 1908 raise qm.cmdline.CommandError, \ 1909 qm.error("invalid results format", 1910 format=format, 1911 valid_formats=valid_format_string) 1912 if format != "none": 1913 args = { "format" : format } 1914 args.update(arguments) 1915 stream = self.GetTextResultStreamClass()(args) 1916 result_streams.append(stream) 1917 1918 f = lambda n: get_extension_class(n, "result_stream", database) 1919 1920 # Look for all of the "--result-stream" options. 1921 for opt, opt_arg in self.__command_options: 1922 if opt == "result-stream": 1923 ec, args = qm.extension.parse_descriptor(opt_arg, f) 1924 args.update(arguments) 1925 result_streams.append(ec(args)) 1926 1927 # If there is an output file, create a standard results file on 1928 # that file. 1929 if output_file is not None: 1930 rs = (self.GetFileResultStreamClass() 1931 ({ "filename" : output_file})) 1932 result_streams.append(rs) 1933 1934 for name, value in annotations.iteritems(): 1935 for rs in result_streams: 1936 rs.WriteAnnotation(name, value) 1937 1938 return result_streams
1939 1940 ######################################################################## 1941 # Functions 1942 ######################################################################## 1943
1944 -def get_qmtest():
1945 """Returns the global QMTest object. 1946 1947 returns -- The 'QMTest' object that corresponds to the currently 1948 executing thread. 1949 1950 At present, there is only one QMTest object per process. In the 1951 future, however, there may be more than one. Then, this function 1952 will return different values in different threads.""" 1953 1954 return _the_qmtest
1955 1956 ######################################################################## 1957 # Local Variables: 1958 # mode: python 1959 # indent-tabs-mode: nil 1960 # fill-column: 72 1961 # End: 1962