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

Source Code for Module qm.test.web.web

   1  ######################################################################## 
   2  # 
   3  # File:   web.py 
   4  # Author: Alex Samuel 
   5  # Date:   2001-04-09 
   6  # 
   7  # Contents: 
   8  #   Common code for QMTest web user interface. 
   9  # 
  10  # Copyright (c) 2001 - 2005 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 
  21  import qm 
  22  import qm.attachment 
  23  import qm.common 
  24  from   qm.extension import * 
  25  import qm.fields 
  26  import qm.label 
  27  import qm.test.base 
  28  import qm.test.cmdline 
  29  from   qm.test.context import * 
  30  from   qm.test.database import * 
  31  from   qm.test.classes.previous_testrun import PreviousTestRun 
  32  from   qm.test.execution_thread import * 
  33  from   qm.test.result import * 
  34  from   qm.test.result_stream import * 
  35  from   qm.test.suite import * 
  36  import qm.web 
  37  import string 
  38  import StringIO 
  39  import sys 
  40  import time 
  41   
  42  ######################################################################## 
  43  # classes 
  44  ######################################################################## 
  45   
46 -class Item:
47 """An 'Item' provides a convenient way to pass around named values. 48 It is iterated over in listings generated by various dtml templates.""" 49
50 - def __init__(self, id, **kwds):
51 """Construct a new 'Item'. 52 53 'id' -- The identifier. 54 55 'kwds' -- A dictionary of named values to store.""" 56 57 self.id = id 58 self.__dict__.update(kwds)
59 60 61
62 -class DefaultDtmlPage(qm.web.DtmlPage):
63 """Subclass of DTML page class for QMTest pages.""" 64 65 html_generator = "QMTest" 66 67 NEGATIVE_UNEXPECTED = Result.FAIL 68 """A test's result was unfavorably unexpected.""" 69 70 POSITIVE_UNEXPECTED = Result.PASS 71 """A test's result was favorably unexpected.""" 72 73 EXPECTED = "EXPECTED" 74 """A test's result was as expected.""" 75 76 EXPECTATION_KINDS \ 77 = [ NEGATIVE_UNEXPECTED, EXPECTED, POSITIVE_UNEXPECTED ] 78 """The kinds of expectations.""" 79 80 outcomes = Result.outcomes + [EXPECTED] 81
82 - def __init__(self, dtml_template, server, **attributes):
83 """Construct a new 'QMTestPage'. 84 85 'server' -- The 'QMTestServer' creating this page. 86 87 'dtml_template' -- The file name of the DTML template, relative 88 to the DTML directory.""" 89 90 self.server = server 91 92 # Set up the menus first; the attributes might override them. 93 if not server.GetRunDatabase(): 94 if server.GetDatabase().IsModifiable(): 95 self.file_menu_items = [ 96 ('New Test', "new-test"), 97 ('New Suite', "new-suite"), 98 ('New Resource', "new-resource"), 99 ] 100 else: 101 self.file_menu_items = [ 102 ('New Test', ""), 103 ('New Suite', ""), 104 ('New Resource', ""), 105 ] 106 self.file_menu_items.extend([ 107 ('Load Results', "javascript:load_results();"), 108 ('Save Results', qm.test.cmdline.QMTest.results_file_name), 109 ('Load Expectations', "javascript:load_expected_results();")]) 110 if self.HasModifiableExpectations(): 111 self.file_menu_items.extend([ 112 ('Save Expectations', 113 qm.test.cmdline.QMTest.expectations_file_name)]) 114 else: 115 self.file_menu_items.extend([('Save Expectations', "")]) 116 117 self.file_menu_items.extend([ 118 ('Load Context', "javascript:load_context();"), 119 ('Save Context', qm.test.cmdline.QMTest.context_file_name), 120 ('Exit', 'shutdown') 121 ]) 122 self.edit_menu_items = [ 123 ('Clear Results', "clear-results"), 124 ('Edit Context', "edit-context"), 125 ] 126 self.run_menu_items = [ 127 ('All Tests', "run-tests") 128 ] 129 self.view_menu_items = [ 130 ('Directory', "dir"), 131 ('Results', "show-results"), 132 ('Report', "") 133 ] 134 else: 135 136 self.file_menu_items = [ 137 ('New Test', ""), 138 ('New Suite', ""), 139 ('New Resource', ""), 140 ('Load Results', ""), 141 ('Save Results', ""), 142 ('Load Expectations', ""), 143 ('Save Expectations', ""), 144 ('Load Context', ""), 145 ('Save Context', ""), 146 ('Exit', 'shutdown') 147 ] 148 self.edit_menu_items = [ 149 ('Clear Results', ""), 150 ('Edit Context', ""), 151 ] 152 self.run_menu_items = [ 153 ('All Tests', "") 154 ] 155 self.view_menu_items = [ 156 ('Directory', "/test/dir"), 157 ('Results', ""), 158 ('Report', "/report/dir") 159 ] 160 161 self.help_menu_items = [ 162 ('Tutorial', "javascript:popup_tutorial();"), 163 ('QMTest Web Site', "http://www.qmtest.com") 164 ] 165 166 qm.web.DtmlPage.__init__(self, dtml_template, **attributes)
167 168
169 - def GetName(self):
170 """Return the name of the application.""" 171 172 return self.html_generator
173 174
175 - def MakeListingUrl(self):
176 return qm.web.WebRequest("dir", base=self.request).AsUrl()
177 178
179 - def GetMainPageUrl(self):
180 return self.MakeListingUrl()
181 182
183 - def GetDatabase(self):
184 """Returns the 'Database' in use. 185 186 returns -- The 'Database' in use.""" 187 188 return self.server.GetDatabase()
189 190
191 - def IsLabelInDirectory(self, id, directory):
192 """Returns true if 'id' is in 'directory'. 193 194 returns -- True if 'id' indicates a test contained in 195 'directory', or one of its subdirectories.""" 196 197 while len(id) >= len(directory): 198 if id == directory: 199 return 1 200 id = self.GetDatabase().SplitLabel(id)[0] 201 202 return 0
203 204
205 - def FormatId(self, id, type, style="basic"):
206 """Format 'id' as HTML. 207 208 'id' -- The name of a test or resource. 209 210 'type' -- The kind of item named by 'id'. Either 'resource', 211 'suite', or 'test'. 212 213 'style' -- The formatting style to use. One of 'plain', 214 'basic', 'navigation', or 'tree'. 215 216 returns -- A string containing HTML to use for 'id'.""" 217 218 script = "show-" + type 219 request = qm.web.WebRequest(script, self.request, True, id=id) 220 url = request.AsUrl() 221 parent_suite_id, name = self.GetDatabase().SplitLabel(id) 222 223 if style == "plain": 224 return '<span class="id">%s</span>' % id 225 226 elif style == "basic": 227 return '<a href="%s"><span class="id">%s</span></a>' % (url, id) 228 229 elif style == "navigation": 230 if parent_suite_id == "": 231 parent = "" 232 else: 233 parent = self.FormatId(parent_suite_id, "dir", style) 234 parent += id[len(parent_suite_id)] 235 return parent \ 236 + '<a href="%s"><span class="id">%s</span></a>' \ 237 % (url, name) 238 239 elif style == "tree": 240 return '<a href="%s"><span class="id">%s</span></a>' \ 241 % (url, name) 242 243 assert None
244 245
246 - def GetResultsByOutcome(self, results):
247 """Compute the tests in 'results' with each outcome. 248 249 'results' -- A sequence of 'Result' instances. 250 251 returns -- A dictionary mapping outcomes to the sequence of 252 tests that have the indicated outcome in 'results'.""" 253 254 results_by_outcome = {} 255 # At first, there are no results for any outcome. 256 for o in self.outcomes: 257 results_by_outcome[o] = [] 258 259 # Iterate through the results, adding each one to 260 # 'results_by_outcome'. 261 for r in results: 262 results_by_outcome[r.GetOutcome()].append(r) 263 264 return results_by_outcome
265 266
267 - def GetOutcomePercentages(self, results):
268 """Compute the percentage (by outcome) of the 'results'. 269 270 'results' -- A sequence of 'Result' instances. 271 272 returns -- A dictionary mapping outcomes to the percentage (as 273 a floating point number) of tests in 'results' that have 274 that outcome.""" 275 276 # Compute the total number of tests for which results are 277 # available. 278 total = len(results) 279 280 # Get the test results, organized by outcome. 281 results = self.GetResultsByOutcome(results) 282 283 # Compute the percentages. 284 percentages = {} 285 for o in self.outcomes: 286 if total: 287 percentages[o] = float(len(results[o])) / float(total) 288 else: 289 percentages[o] = 0.0 290 291 return percentages
292
293 - def HasModifiableExpectations(self):
294 """Return True if expectations are modifiable.""" 295 296 return type(self.server.GetExpectationDatabase()) in (PreviousTestRun,)
297 298
299 -class QMTestPage(DefaultDtmlPage):
300 """A 'QMTestPage' is a 'DtmlPage' for pages generated by QMTest. 301 302 A 'QMTestPage' automatically looks for DTML templates in the 303 directory that contains QMTest DTML templates.""" 304
305 - def __init__(self, dtml_template, server):
306 """Construct a new 'QMTestPage'. 307 308 'dtml_template' -- The file name of the DTML template, relative 309 to the directory that contains QMTest DTML templates. (Usually, 310 this is just a basename.) 311 312 'server' -- The 'QMTestServer' creating this page.""" 313 314 # Initialize the base class. 315 DefaultDtmlPage.__init__(self, 316 os.path.join("test", dtml_template), 317 server) 318 319 # Make the QMTest object available to the DTML pages. 320 self.qmtest = qm.test.cmdline.get_qmtest()
321 322
323 - def GenerateStartBody(self, decorations=1):
324 if decorations: 325 # If the server is in the midst of executing tests, it 326 # is not safe to edit tests, or to rerun the tests. 327 if not self.server.GetResultsStream().IsFinished(): 328 # The basic edit menu items are OK. 329 edit_menu_items = self.edit_menu_items[0:2] 330 # The run model should have no options. 331 run_menu_items = [ 332 ('Stop Tests', "stop-tests") 333 ] 334 # Otherwise, just use the values specified. 335 else: 336 edit_menu_items = self.edit_menu_items 337 run_menu_items = self.run_menu_items 338 339 # Figure out whether to use click-to-activate menus. 340 click_menus = 0 341 if qm.common.rc.has_option("common", "click_menus"): 342 try: 343 click_menus = qm.common.rc.getboolean("common", 344 "click_menus") 345 except ValueError: 346 pass 347 348 # Generate the navigation bar. 349 navigation_bar = \ 350 DefaultDtmlPage(os.path.join("test", "navigation-bar.dtml"), 351 self.server, 352 file_menu_items=self.file_menu_items, 353 edit_menu_items=edit_menu_items, 354 view_menu_items=self.view_menu_items, 355 run_menu_items=run_menu_items, 356 help_menu_items=self.help_menu_items, 357 click_menus = click_menus) 358 return "<body>%s<br />" % navigation_bar(self.request) 359 else: 360 return "<body>"
361 362 363
364 - def IsFinished(self):
365 """Return true iff no more results are forthcoming. 366 367 returns -- True if no more tests are running.""" 368 369 return 1
370 371
372 - def GetRefreshDelay(self):
373 """Returns the number of seconds to wait before refreshing the page. 374 375 returns -- The number of seconds to wait before refreshing this 376 page. A value of zero means that te page should never be 377 refreshed. This function is only called if 'IsFinished' returns 378 true.""" 379 380 return 0
381 382
383 - def GenerateHtmlHeader(self, description, headers=""):
384 """Return the header for an HTML document. 385 386 'description' -- A string describing this page. 387 388 'headers' -- Any additional HTML headers to place in the 389 '<head>' section of the HTML document.""" 390 391 # If the page isn't finished, automatically refresh it 392 # every few seconds.y 393 if not self.IsFinished(): 394 headers = (headers 395 + ('<meta http-equiv="refresh" content="%d" />' 396 % self.GetRefreshDelay())) 397 398 return DefaultDtmlPage.GenerateHtmlHeader(self, description, 399 headers)
400 401
402 - def GetExpectationUrl(self, id, expectation):
403 """Return the URL for setting the expectation associated with 'id'. 404 405 'id' -- The name of a test. 406 407 'expectation' -- The current expectation associated with the 408 test, or 'None' if there is no associated expectation.""" 409 410 return qm.web.WebRequest("set-expectation", 411 base=self.request, 412 id=id, 413 expectation=expectation or "None", 414 url=self.request.AsUrl()).AsUrl()
415 416 417 418
419 -class QMTestReportPage(DefaultDtmlPage):
420 """A 'QMTestReportPage' is a 'DtmlPage' for pages generated by QMTest. 421 422 A 'QMTestReportPage' automatically looks for DTML templates in the 423 directory that contains QMTest DTML templates.""" 424
425 - def __init__(self, dtml_template, server):
426 """Construct a new 'QMTestReportPage'. 427 428 'dtml_template' -- The file name of the DTML template, relative 429 to the directory that contains QMTest DTML templates. (Usually, 430 this is just a basename.) 431 432 'server' -- The 'QMTestServer' creating this page.""" 433 434 # Initialize the base class. 435 DefaultDtmlPage.__init__(self, 436 os.path.join("report", dtml_template), 437 server) 438 # Make the QMTest object available to the DTML pages. 439 self.qmtest = qm.test.cmdline.get_qmtest()
440 441
442 - def GetRunDatabase(self):
443 """Returns the 'RunDatabase' in use. 444 445 returns -- The 'RunDatabase' in use.""" 446 447 return self.server.GetRunDatabase()
448 449
450 - def GenerateStartBody(self, decorations=1):
451 if decorations: 452 edit_menu_items = self.edit_menu_items 453 run_menu_items = self.run_menu_items 454 455 # Figure out whether to use click-to-activate menus. 456 click_menus = 0 457 if qm.common.rc.has_option("common", "click_menus"): 458 try: 459 click_menus = qm.common.rc.getboolean("common", 460 "click_menus") 461 except ValueError: 462 pass 463 464 # Generate the navigation bar. 465 navigation_bar = \ 466 DefaultDtmlPage(os.path.join("test", "navigation-bar.dtml"), 467 self.server, 468 file_menu_items=self.file_menu_items, 469 edit_menu_items=edit_menu_items, 470 view_menu_items=self.view_menu_items, 471 run_menu_items=run_menu_items, 472 help_menu_items=self.help_menu_items, 473 click_menus = click_menus) 474 return "<body>%s<br />" % navigation_bar(self.request) 475 else: 476 return "<body>"
477 478 479
480 - def GenerateHtmlHeader(self, description, headers=""):
481 """Return the header for an HTML document. 482 483 'description' -- A string describing this page. 484 485 'headers' -- Any additional HTML headers to place in the 486 '<head>' section of the HTML document.""" 487 488 return DefaultDtmlPage.GenerateHtmlHeader(self, description, 489 headers)
490 491
492 - def GetResultURL(self, id, kind):
493 """Generate a URL for the result page for 'id'. 494 495 'id' -- The name of a test or resource. 496 497 'kind' -- either 'test' or 'resource'. 498 499 returns -- A url string for the result page for 'id'.""" 500 501 row = self.request.get('row', 'qmtest.run.uname') 502 script = {"test": "show-test", 503 "resource": "show-resource"}[kind] 504 request = qm.web.WebRequest(script, self.request, True, 505 id=id, 506 row=row) 507 return request.AsUrl()
508 509
510 - def FormatTimeIso(self, time):
511 512 return qm.common.format_time_iso(time)
513 514 515
516 -class ContextPage(QMTestPage):
517 """DTML page for setting the context.""" 518
519 - def __init__(self, server):
520 """Construct a new 'ContextPage'. 521 522 'server' -- The 'QMTestServer' creating this page.""" 523 524 QMTestPage.__init__(self, "context.dtml", server) 525 526 self.context = server.GetContext()
527 528 529
530 -class DirPage(QMTestPage):
531 """A test database directory page. 532 533 These attributes are available in DTML: 534 535 'path' -- The label directory that is being displayed. 536 537 'subdirs' -- A sequence of labels giving the subdirectories of 538 this directory. 539 540 'test_ids' -- A sequence of labels giving the tests in this 541 directory. 542 543 'suite_ids' -- A sequence of labels giving the suites in this 544 directory. 545 546 'resource_ids' -- A sequence of labels giving the resources in 547 this directory.""" 548 549 SORT_NAME = 'name' 550 """Sort by name.""" 551 552 SORT_OUTCOME = 'outcome' 553 """Sort by outcome.""" 554 555 SORT_EXPECTATION = 'expectation' 556 """Sort by expectation. In other words, put unexpected outcomes 557 before expected outcomes.""" 558 559 SORT_KINDS = [ SORT_NAME, SORT_OUTCOME, SORT_EXPECTATION ] 560 """The kinds of sorting available.""" 561
562 - def __init__(self, server, path):
563 """Construct a 'DirPage'. 564 565 'server' -- The 'QMTestServer' creating this page. 566 567 'path' -- The label directory to display.""" 568 569 # Initialize the base class. 570 QMTestPage.__init__(self, "dir.dtml", server) 571 572 self.path = path 573 self.database = server.GetDatabase() 574 self.subdir_ids = self.database.GetSubdirectories(path) 575 self.subdir_ids = map(lambda l: self.database.JoinLabels(path, l), 576 self.subdir_ids) 577 self.test_ids = self.database.GetTestIds(path, scan_subdirs=0) 578 self.suite_ids = self.database.GetSuiteIds(path, scan_subdirs=0) 579 # Do not show implicit suites. Otherwise, there are two 580 # entries for a directory: one as a subdirectory entry, and 581 # the other as a test suite. 582 self.suite_ids = filter(lambda s, d=self.database: \ 583 not d.GetSuite(s).IsImplicit(), 584 self.suite_ids) 585 self.resource_ids = self.database.GetResourceIds(path, scan_subdirs=0) 586 587 # Get the results to date. 588 results_stream = server.GetResultsStream() 589 # It is important that we ask for IsFinished before asking 590 # for GetTestResults. The stream could be finished between 591 # the two calls, and it is better to show all the results but 592 # claim they are incomplete than to show only some of the 593 # results and claim they are complete. 594 self.__is_finished = results_stream.IsFinished() 595 self.test_results = results_stream.GetTestResults() 596 self.expected_outcomes = server.GetExpectedOutcomes() 597 self.expectation_is_modifiable = \ 598 type(server.GetExpectationDatabase()) in (PreviousTestRun,) 599 600 # Make it easy for the DTML page to get at all the outcomes. 601 #self.outcomes = Result.outcomes + [self.EXPECTED] 602 603 # Provide a menu choice to allow running all of the tests in 604 # this directory. 605 if self.server.GetRunDatabase(): 606 self.run_menu_items.append(("This Directory", "")) 607 else: 608 self.run_menu_items.append(("This Directory", "javascript:run_dir();"))
609 610
611 - def GetRunUrl(self):
612 """Return the URL for running this directory.""" 613 614 return qm.web.WebRequest("run-tests", 615 self.request, 616 ids=self.path).AsUrl()
617 618
619 - def GetTestResultsForDirectory(self, directory):
620 """Return all of the test results for tests in 'directory'. 621 622 'directory' -- A string giving the label for a directory. 623 624 returns -- A sequence of 'Result' instances corresponding to 625 results for tests from the indicated directory.""" 626 627 # If we are in report mode, fetch the results from the run database. 628 test_run = self.request.get('test_run') 629 run_db = self.server.GetRunDatabase() 630 if test_run and run_db: 631 return run_db.GetAllRuns()[int(test_run)].GetAllResults(directory) 632 633 # Else use the test_results. 634 if directory == "": 635 return self.test_results.values() 636 else: 637 return [r for r in self.test_results.values() 638 if self.IsLabelInDirectory(r.GetId(), directory)]
639 640
641 - def GetUnexpectedResultsByOutcome(self, results):
642 """Compute the tests in 'results' with each outcome. 643 644 'results' -- A sequence of 'Result' instances. 645 646 returns -- A dictionary mapping outcomes to the results with 647 that outcome -- and for which that outcome is unexpected. 648 The (fake) outcome 'self.EXPECTED' is mapped to expected 649 results.""" 650 651 results_by_outcome = {} 652 # At first, there are no results for any outcome. 653 for o in self.outcomes: 654 results_by_outcome[o] = [] 655 656 for r in results: 657 # See what outcome was expected. 658 expectation = self.GetExpectation(r.GetId()) or Result.PASS 659 # Update results_by_outcome. 660 if r.GetOutcome() != expectation: 661 results_by_outcome[r.GetOutcome()].append(r) 662 else: 663 results_by_outcome[self.EXPECTED].append(r) 664 665 return results_by_outcome
666 667
668 - def GetUnexpectedOutcomePercentages(self, results):
669 """Compute percentages of unexpected 'results'. 670 671 'results' -- A sequence of 'Result' instances. 672 673 returns -- A dictionary mapping the 'EXPECTATION_KINDS' to the 674 percentage (as a floating point number) of tests in 'results' 675 that have that expectation.""" 676 677 # Compute the total number of tests for which results are 678 # available. 679 total = len(results) 680 681 # Get the test results, organized by outcome. 682 results_by_outcome \ 683 = self.GetUnexpectedResultsByOutcome(results) 684 685 # Compute the absolute number of tests in each category. 686 percentages = {} 687 percentages[self.POSITIVE_UNEXPECTED] \ 688 = len(results_by_outcome[Result.PASS]) 689 percentages[self.NEGATIVE_UNEXPECTED] \ 690 = (len(results_by_outcome[Result.FAIL]) 691 + len(results_by_outcome[Result.ERROR]) 692 + len(results_by_outcome[Result.UNTESTED])) 693 percentages[self.EXPECTED] \ 694 = len(results_by_outcome[self.EXPECTED]) 695 696 # And the corresponding percentages. 697 for e in self.EXPECTATION_KINDS: 698 if percentages[e]: 699 percentages[e] = float(percentages[e]) / float(total) 700 else: 701 percentages[e] = 0.0 702 703 return percentages
704 705
706 - def CountUnexpected(self, results):
707 """Count the unexpected 'results'. 708 709 'results' -- A dictionary of the form returned by 710 'GetUnexpectedResultsByOutcome'. 711 712 returns -- The total number of unexpected results.""" 713 714 total = 0 715 # Go through all the outcomes except 'EXPECTED'. 716 for o in Result.outcomes: 717 total += len(results[o]) 718 719 return total
720 721
722 - def GetResultURL(self, id, kind):
723 """Generate a URL for the result page for 'id'. 724 725 'id' -- The name of a test or resource. 726 727 'kind' -- either 'test' or 'resource'. 728 729 returns -- A url string for the result page for 'id'.""" 730 731 script = {"test": "show-test", 732 "resource": "show-resource"}[kind] 733 request = qm.web.WebRequest(script, base=self.request, id=id) 734 return request.AsUrl()
735 736
737 - def GetTests(self, sort):
738 """Return information about all of the tests. 739 740 'sort' -- One of the 'SORT_KINDS' indicating how the results 741 should be sorted. 742 743 returns -- A sequence of 'Item' instances 744 corresponding to all of the tests in this diretory.""" 745 746 # There is no information yet. 747 tests = [] 748 749 # Iterate through each of the tests. 750 for id in self.test_ids: 751 outcome = self.GetTestOutcome(id) 752 expectation = self.GetExpectation(id) 753 tests.append(Item(id, 754 outcome=outcome, 755 expectation=expectation)) 756 757 if sort == self.SORT_NAME: 758 # The tests are already sorted by name. 759 pass 760 elif sort == self.SORT_OUTCOME: 761 # Sort the test by outcome; interesting outcomes come first. 762 buckets = {} 763 for o in Result.outcomes + [None]: 764 buckets[o] = [] 765 766 # Go through the tests dropping each in the right bucket. 767 for t in tests: 768 buckets[t.outcome].append(t) 769 770 # Combine the buckets. 771 tests = [] 772 for o in Result.outcomes + [None]: 773 tests += buckets[o] 774 elif sort == self.SORT_EXPECTATION: 775 # Sort the test by expectations; unexpected outcomes come 776 # first. 777 buckets = {} 778 for o in ['UNEXPECTED', self.EXPECTED, None]: 779 buckets[o] = [] 780 781 # Go through the tests dropping each in the right bucket. 782 for t in tests: 783 if (t.outcome == (t.expectation or Result.PASS)): 784 buckets[self.EXPECTED].append(t) 785 elif t.outcome: 786 buckets['UNEXPECTED'].append(t) 787 else: 788 buckets[None].append(t) 789 790 # Combine the buckets. 791 tests = [] 792 for o in ['UNEXPECTED', self.EXPECTED, None]: 793 tests += buckets[o] 794 else: 795 # Ignore the sort request. (We cannot assert that this case 796 # never happens because users can type any URL they like 797 # into their web browser.) 798 pass 799 800 return tests
801 802
803 - def GetTestOutcome(self, test_id):
804 """Return the 'Result' for 'test_id'. 805 806 'test_id' -- The name of the test whose result is requested. 807 808 'result' -- The result associated with the 'test_id', or 809 'None' if no result is available.""" 810 811 test_run = self.request.get('test_run') 812 run_db = self.server.GetRunDatabase() 813 if test_run and run_db: 814 result = run_db.GetAllRuns()[int(test_run)].GetResult(test_id) 815 else: 816 result = self.test_results.get(test_id) 817 return result and result.GetOutcome()
818 819
820 - def GetDetailURL(self, test_id):
821 """Return the detail URL for 'test_id'. 822 823 'test_id' -- The name of the test. 824 825 returns -- The URL that contains details about the 'test_id'.""" 826 827 return qm.web.WebRequest("show-result", 828 self.request, 829 True, 830 id=test_id).AsUrl()
831 832
833 - def GetExpectation(self, test_id):
834 """Return the expected outcome for 'test_id'. 835 836 'test_id' -- The name of the test. 837 838 returns -- A string giving the expected outcome for 'test_id', 839 or 'None' if there is no expectation.""" 840 841 return self.expected_outcomes.get(test_id)
842 843
844 - def GetSortURL(self, sort):
845 """Get the URL for this page, but sorted as indicated. 846 847 'sort' -- One of the 'SORT_KINDS'. 848 849 returns -- A URL indicating this page, but sorted as 850 indicated.""" 851 852 return qm.web.WebRequest("show-dir", 853 base=self.request, 854 id=self.path, 855 sort=sort).AsUrl()
856
857 - def IsFinished(self):
858 """Return true iff no more results are forthcoming. 859 860 returns -- True if no more tests are running.""" 861 862 return self.__is_finished
863 864
865 - def GetRefreshDelay(self):
866 """Returns the number of seconds to wait before refreshing the page. 867 868 returns -- The number of seconds to wait before refreshing this 869 page. A value of zero means that te page should never be 870 refreshed. This function is only called if 'IsFinished' returns 871 true.""" 872 873 if len(self.test_results.items()) < 50: 874 return 10 875 else: 876 return 30
877 878
879 -class DirReportPage(QMTestReportPage):
880 """A run database directory page. 881 882 These attributes are available in DTML: 883 884 'path' -- The label directory that is being displayed. 885 886 'subdirs' -- A sequence of labels giving the subdirectories of 887 this directory. 888 889 'test_ids' -- A sequence of labels giving the tests in this 890 directory. 891 892 'suite_ids' -- A sequence of labels giving the suites in this 893 directory. 894 895 'resource_ids' -- A sequence of labels giving the resources in 896 this directory.""" 897 898 SORT_NAME = 'name' 899 """Sort by name.""" 900 901 SORT_OUTCOME = 'outcome' 902 """Sort by outcome.""" 903 904 SORT_EXPECTATION = 'expectation' 905 """Sort by expectation. In other words, put unexpected outcomes 906 before expected outcomes.""" 907 908 SORT_KINDS = [ SORT_NAME, SORT_OUTCOME, SORT_EXPECTATION ] 909 """The kinds of sorting available.""" 910
911 - def __init__(self, server, path):
912 """Construct a 'DirPage'. 913 914 'server' -- The 'QMTestServer' creating this page. 915 916 'path' -- The label directory to display.""" 917 918 # Initialize the base class. 919 QMTestReportPage.__init__(self, "dir.dtml", server) 920 921 self.path = path 922 database = server.GetDatabase() 923 self.database = database 924 self.run_db = server.GetRunDatabase() 925 self.subdir_ids = [database.JoinLabels(path, l) 926 for l in database.GetSubdirectories(path)] 927 self.test_ids = self.database.GetTestIds(path, scan_subdirs=0) 928 self.suite_ids = [s for s in database.GetSuiteIds(path, scan_subdirs=0) 929 if not database.GetSuite(s).IsImplicit()] 930 self.resource_ids = self.database.GetResourceIds(path, scan_subdirs=0)
931 932
933 - def GetItems(self, kind = Result.TEST):
934 """Return information about all of the items. 935 936 returns -- A sequence of 'Item' instances 937 corresponding to all of the tests in this diretory.""" 938 939 # There is no information yet. 940 items = [] 941 942 # Iterate through each of the tests. 943 test_runs = len(self.run_db.GetAllRuns()) 944 for id in self.test_ids: 945 outcomes = self.run_db.GetOutcomes(id, kind) 946 if outcomes[Result.PASS] == test_runs: 947 outcome = Result.PASS 948 outcome_class = 'qmtest_pass' 949 elif outcomes[Result.FAIL] == test_runs: 950 outcome = Result.FAIL 951 outcome_class = 'qmtest_fail' 952 elif outcomes[Result.UNTESTED] == test_runs: 953 outcome = Result.UNTESTED 954 outcome_class = 'qmtest_untested' 955 elif outcomes[Result.ERROR] == test_runs: 956 outcome = Result.ERROR 957 outcome_class = 'qmtest_error' 958 else: 959 outcome = 'MIXED' 960 outcome_class = 'qmtest_mixed' # FIXME: add more detail here 961 items.append(Item(id, 962 outcome=outcome, 963 outcome_class=outcome_class)) 964 965 return items
966 967
968 - def MakeTestRunUrl(self, test_run):
969 """Return the URL for navigating a particular test run.""" 970 971 return qm.web.WebRequest("/test/dir", 972 self.request, True, 973 test_run=test_run, 974 id=self.path).AsUrl()
975 976 977 978
979 -class ShowItemReportPage(QMTestReportPage):
980 """DTML page for showing tests and resources.""" 981
982 - def __init__(self, server, item, type, field_errors={}):
983 """Construct a new DTML context. 984 985 These parameters are also available in DTML under the same name: 986 987 'server' -- The 'QMTestServer' creating this page. 988 989 'item' -- The 'TestDescriptor' or 'ResourceDescriptor' for the 990 test being shown. 991 992 'type' -- Either "test" or "resource". 993 994 'field_errors' -- A map from field names to corresponding error 995 messages.""" 996 997 # Initialize the base class. 998 QMTestReportPage.__init__(self, "show.dtml", server) 999 self.item = item 1000 self.fields = item.GetClassArguments() 1001 assert type in ["test", "resource"] 1002 self.type = type 1003 self.field_errors = field_errors
1004 1005
1006 - def GetTitle(self):
1007 """Return the page title for this page.""" 1008 1009 url = self.request.GetScriptName() 1010 title = {"show-test": "Show Test Report ", 1011 "show-resource": "Show Resource Report "}[url] 1012 return title + self.item.GetId()
1013 1014
1015 - def FormatFieldValue(self, field):
1016 """Return an HTML rendering of the value for 'field'.""" 1017 1018 # Extract the field value. 1019 arguments = self.item.GetArguments() 1020 field_name = field.GetName() 1021 try: 1022 value = arguments[field_name] 1023 except KeyError: 1024 # Use the default value if none is provided. 1025 value = field.GetDefaultValue() 1026 return field.FormatValueAsHtml(self.server, value, "full")
1027 1028
1029 - def GetClassDescription(self):
1030 """Return a full description of the test or resource class. 1031 1032 returns -- The description, formatted as HTML.""" 1033 1034 d = qm.extension.get_class_description(self.item.GetClass()) 1035 return qm.web.format_structured_text(d)
1036 1037
1038 - def GetBriefClassDescription(self):
1039 """Return a brief description of the test or resource class. 1040 1041 returns -- The brief description, formatted as HTML.""" 1042 1043 d = qm.extension.get_class_description(self.item.GetClass(), 1044 brief=1) 1045 return qm.web.format_structured_text(d)
1046 1047
1048 - def MakeShowUrl(self):
1049 """Return the URL for showing this item.""" 1050 1051 return qm.web.WebRequest("show-" + self.type, 1052 self.request, True, 1053 id=self.item.GetId()).AsUrl()
1054 1055
1056 - def GetDetailUrl(self, test_run):
1057 """Return the detail URL for a test. 1058 1059 'test_id' -- The name of the test. 1060 1061 returns -- The URL that contains details about the 'test_id'.""" 1062 1063 return qm.web.WebRequest("show-result", 1064 self.request, True, 1065 id=self.item.GetId(), 1066 test_run=test_run).AsUrl()
1067 1068
1069 - def GetResults(self, key=None):
1070 """Return the results from all runs that correpond to the current id.""" 1071 1072 result_set = [] 1073 test_runs = self.server.GetRunDatabase().GetAllRuns() 1074 for r in range(len(test_runs)): 1075 k = test_runs[r].GetAnnotation(key or self.request['row']) 1076 result = None 1077 try: 1078 result = test_runs[r].GetResult(self.item.GetId(), self.type) 1079 except KeyError: 1080 pass 1081 result_set.append(Item(k, test_run=r, result=result)) 1082 return result_set
1083 1084 1085
1086 -class LoadContextPage(QMTestPage):
1087 """DTML page for uploading a context.""" 1088 1089 title = "Load Context" 1090 """The title for the page.""" 1091 1092 heading = "Load the context from a file." 1093 """The heading printed across the top of the page.""" 1094 1095 prompt = "The file from which to load the context." 1096 """The prompt for the file name.""" 1097 1098 submit_url = "submit-context-file" 1099 """The URL to which the file should be submitted.""" 1100
1101 - def __init__(self, server):
1102 """Construct a new 'LoadContextPage'. 1103 1104 'server' -- The 'QMTestServer' creating this page.""" 1105 1106 QMTestPage.__init__(self, "load.dtml", server)
1107 1108 1109
1110 -class LoadExpectationsPage(QMTestPage):
1111 """DTML page for uploading a context.""" 1112 1113 title = "Load Expectations" 1114 """The title for the page.""" 1115 1116 heading = "Load expectations from a file." 1117 """The heading printed across the top of the page.""" 1118 1119 prompt = "The file from which to load expectations.""" 1120 """The prompt for the file name.""" 1121 1122 submit_url = "submit-expectations" 1123 """The URL to which the file should be submitted.""" 1124
1125 - def __init__(self, server):
1126 """Construct a new 'LoadExpectationsPage'. 1127 1128 'server' -- The 'QMTestServer' creating this page.""" 1129 1130 QMTestPage.__init__(self, "load.dtml", server)
1131 1132 1133
1134 -class LoadResultsPage(QMTestPage):
1135 """DTML page for uploading a context.""" 1136 1137 title = "Load Results" 1138 """The title for the page.""" 1139 1140 heading = "Load results from a file." 1141 """The heading printed across the top of the page.""" 1142 1143 prompt = "The file from which to load the results.""" 1144 """The prompt for the file name.""" 1145 1146 submit_url = "submit-results" 1147 """The URL to which the file should be submitted.""" 1148
1149 - def __init__(self, server):
1150 """Construct a new 'LoadContextPage'. 1151 1152 'server' -- The 'QMTestServer' creating this page.""" 1153 1154 QMTestPage.__init__(self, "load.dtml", server)
1155 1156 1157
1158 -class NewItemPage(QMTestPage):
1159 """Page for creating a new test or resource.""" 1160
1161 - def __init__(self, 1162 server, 1163 type, 1164 item_id="", 1165 class_name="", 1166 field_errors={}):
1167 """Create a new DTML context. 1168 1169 'type' -- Either "test" or "resource". 1170 1171 'server' -- The 'QMTestServer' creating this page. 1172 1173 'item_id' -- The item ID to show. 1174 1175 'class_name' -- The class name to show. 1176 1177 'field_errors' -- A mapping of error messages for fields. Keys 1178 may be "_id" or "_class".""" 1179 1180 # Initialize the base class. 1181 QMTestPage.__init__(self, "new.dtml", server) 1182 # Set up attributes. 1183 assert type in ["test", "resource"] 1184 self.database = server.GetDatabase() 1185 self.type = type 1186 self.item_id = item_id 1187 self.class_name = class_name 1188 if type == "test": 1189 self.class_names = self.database.GetTestClassNames() 1190 elif type == "resource": 1191 self.class_names = self.database.GetResourceClassNames() 1192 self.field_errors = field_errors
1193 1194
1195 - def GetTitle(self):
1196 """Return the title this page.""" 1197 1198 return "Create a New %s" % string.capwords(self.type)
1199 1200
1201 - def GetClassDescriptions(self):
1202 """Return a description of the available classes. 1203 1204 returns -- Structured text describing each of the available 1205 test or resource classes.""" 1206 1207 desc = "**Available Classes**\n\n" 1208 for n in self.class_names: 1209 c = qm.test.base.get_extension_class(n, self.type, 1210 self.database) 1211 d = qm.extension.get_class_description(c, brief=1) 1212 desc = desc + " * " + n + "\n\n " + d + "\n\n" 1213 1214 return desc
1215 1216
1217 - def MakeSubmitUrl(self):
1218 """Return the URL for submitting the form. 1219 1220 The URL is for the script 'create-test' or 'create-resource' as 1221 appropriate.""" 1222 1223 return qm.web.WebRequest("create-" + self.type, 1224 base=self.request).AsUrl()
1225 1226 1227
1228 -class NewSuitePage(QMTestPage):
1229 """Page for creating a new test suite.""" 1230
1231 - def __init__(self, server, suite_id="", field_errors={}):
1232 """Create a new DTML context. 1233 1234 'server' -- The 'QMTestServer' creating this page. 1235 1236 'suite_id' -- Initial value for the new test suite ID field. 1237 1238 'field_errors' -- A mapping of error messages to fields. If 1239 empty, there are no errors.""" 1240 1241 # Initialize the base class. 1242 QMTestPage.__init__(self, "new-suite.dtml", server) 1243 # Set up attributes. 1244 self.suite_id = suite_id 1245 self.field_errors = field_errors
1246 1247 1248
1249 -class ResultPage(QMTestPage):
1250 """DTML page for showing result detail.""" 1251
1252 - def __init__(self, server, result):
1253 """Construct a new 'ResultPage' 1254 1255 'server' -- The 'QMTestServer' creating this page. 1256 1257 'result' -- The result to display.""" 1258 1259 QMTestPage.__init__(self, "result.dtml", server) 1260 self.result = result 1261 if result.GetKind() == Result.TEST: 1262 self.run_menu_items.append(("This Test", 1263 "javascript:run_test();"))
1264
1265 - def GetResultURL(self, id):
1266 1267 return qm.web.WebRequest("show-result", 1268 base = self.request, 1269 id = id).AsUrl()
1270 1271
1272 - def GetRunURL(self):
1273 1274 return qm.web.WebRequest("run-tests", 1275 base = self.request, 1276 ids = self.result.GetId()).AsUrl()
1277 1278 1279
1280 -class SetExpectationPage(QMTestPage):
1281 """DTML page for setting the expectation associated with a test.""" 1282
1283 - def __init__(self, server, id):
1284 """Construct a new 'SetExpectationPage'. 1285 1286 'server' -- The 'QMTestServer' creating this page. 1287 1288 'id' -- The name of the test whose expectation is being set.""" 1289 1290 QMTestPage.__init__(self, "set-expectation.dtml", server) 1291 self.outcomes = ["None"] + Result.outcomes
1292 1293 1294
1295 -class ShowItemPage(QMTestPage):
1296 """DTML page for showing and editing tests and resources.""" 1297
1298 - def __init__(self, server, item, edit, new, type, field_errors={}):
1299 """Construct a new DTML context. 1300 1301 These parameters are also available in DTML under the same name: 1302 1303 'server' -- The 'QMTestServer' creating this page. 1304 1305 'item' -- The 'TestDescriptor' or 'ResourceDescriptor' for the 1306 test being shown. 1307 1308 'edit' -- True for editing the item; false for displaying it 1309 only. 1310 1311 'new' -- True for editing a newly-created item ('edit' is then 1312 also true). 1313 1314 'type' -- Either "test" or "resource". 1315 1316 'field_errors' -- A map from field names to corresponding error 1317 messages.""" 1318 1319 # Initialize the base class. 1320 QMTestPage.__init__(self, "show.dtml", server) 1321 # Set up attributes. 1322 self.__database = server.GetDatabase() 1323 self.item = item 1324 self.fields = item.GetClassArguments() 1325 self.edit = edit 1326 self.new = new 1327 assert type in ["test", "resource"] 1328 self.type = type 1329 self.field_errors = field_errors 1330 1331 if self.__database.IsModifiable(): 1332 self.edit_menu_items.append(("Edit %s" % string.capitalize(type), 1333 "javascript:edit_item();")) 1334 self.edit_menu_items.append(("Delete %s" % string.capitalize(type), 1335 "javascript:delete_item();")) 1336 1337 if type == "test" and not edit: 1338 self.run_menu_items.append(("This Test", "javascript:run_test();"))
1339 1340
1341 - def GetTitle(self):
1342 """Return the page title for this page.""" 1343 1344 # Map the scriptname to a nicely-formatted title. 1345 url = self.request.GetScriptName() 1346 title = { 1347 "show-test": "Show Test", 1348 "edit-test": "Edit Test", 1349 "create-test": "New Test", 1350 "show-resource": "Show Resource", 1351 "edit-resource": "Edit Resource", 1352 "create-resource": "New Resource", 1353 }[url] 1354 # Show the item's ID too. 1355 title = title + " " + self.item.GetId() 1356 return title
1357 1358
1359 - def FormatFieldValue(self, field):
1360 """Return an HTML rendering of the value for 'field'.""" 1361 1362 # Extract the field value. 1363 arguments = self.item.GetArguments() 1364 field_name = field.GetName() 1365 try: 1366 value = arguments[field_name] 1367 except KeyError: 1368 # Use the default value if none is provided. 1369 value = field.GetDefaultValue() 1370 # Format it appropriately. 1371 server = self.server 1372 if self.edit: 1373 if field.IsHidden(): 1374 return field.FormatValueAsHtml(server, value, "hidden") 1375 elif field.IsReadOnly(): 1376 # For read-only fields, we still need a form input, but 1377 # the user shouldn't be able to change anything. Use a 1378 # hidden input, and display the contents as if this 1379 # wasn't an editing form. 1380 return field.FormatValueAsHtml(server, value, "hidden") \ 1381 + field.FormatValueAsHtml(server, value, "full") 1382 else: 1383 return field.FormatValueAsHtml(server, value, "edit") 1384 else: 1385 return field.FormatValueAsHtml(server, value, "full")
1386 1387
1388 - def GetClassDescription(self):
1389 """Return a full description of the test or resource class. 1390 1391 returns -- The description, formatted as HTML.""" 1392 1393 d = qm.extension.get_class_description(self.item.GetClass()) 1394 return qm.web.format_structured_text(d)
1395 1396
1397 - def GetBriefClassDescription(self):
1398 """Return a brief description of the test or resource class. 1399 1400 returns -- The brief description, formatted as HTML.""" 1401 1402 d = qm.extension.get_class_description(self.item.GetClass(), 1403 brief=1) 1404 return qm.web.format_structured_text(d)
1405 1406
1407 - def MakeEditUrl(self):
1408 """Return the URL for editing this item.""" 1409 1410 return qm.web.WebRequest("edit-" + self.type, 1411 base=self.request, 1412 id=self.item.GetId()).AsUrl()
1413 1414
1415 - def MakeRunUrl(self):
1416 """Return the URL for running this item.""" 1417 1418 return qm.web.WebRequest("run-tests", 1419 base=self.request, 1420 ids=self.item.GetId()).AsUrl()
1421 1422
1423 - def MakeShowUrl(self):
1424 """Return the URL for showing this item.""" 1425 1426 return qm.web.WebRequest("show-" + self.type, 1427 base=self.request, 1428 id=self.item.GetId()).AsUrl()
1429 1430
1431 - def MakeSubmitUrl(self):
1432 """Return the URL for submitting edits.""" 1433 1434 return qm.web.WebRequest("submit-" + self.type, 1435 base=self.request).AsUrl()
1436 1437
1438 - def MakeDeleteScript(self):
1439 """Make a script to confirm deletion of the test or resource. 1440 1441 returns -- JavaScript source to handle deletion of the 1442 test or resource.""" 1443 1444 item_id = self.item.GetId() 1445 delete_url = qm.web.make_url("delete-" + self.type, 1446 base_request=self.request, 1447 id=item_id) 1448 message = """ 1449 <p>Are you sure you want to delete the %s %s?</p> 1450 """ % (self.type, item_id) 1451 return self.server.MakeConfirmationDialog(message, delete_url)
1452 1453 1454
1455 -class ShowSuitePage(QMTestPage):
1456 """Page for displaying the contents of a test suite.""" 1457
1458 - def __init__(self, server, suite, edit, is_new_suite):
1459 """Construct a new DTML context. 1460 1461 'server' -- The 'QMTestServer' creating this page. 1462 1463 'suite' -- The 'Suite' instance to display. 1464 1465 'edit' -- If true, display controls for editing the suite. 1466 1467 'is_new_suite' -- If true, the suite being displayed is being 1468 created at this time.""" 1469 1470 # Initialize the base class. 1471 QMTestPage.__init__(self, "suite.dtml", server) 1472 1473 # It does not make sense to display a new suite without being 1474 # able to edit it; there is nothing to show. 1475 assert edit or not is_new_suite 1476 1477 # Set up attributes. 1478 database = server.GetDatabase() 1479 self.suite = suite 1480 self.test_ids = suite.GetTestIds() 1481 self.suite_ids = suite.GetSuiteIds() 1482 self.edit = edit 1483 self.is_new_suite = is_new_suite 1484 1485 if not suite.IsImplicit() and database.IsModifiable(): 1486 self.edit_menu_items.append(("Edit Suite", 1487 "javascript:edit_suite();")) 1488 self.edit_menu_items.append(("Delete Suite", 1489 "javascript:delete_suite();")) 1490 1491 if not edit: 1492 self.run_menu_items.append(("This Suite", 1493 "javascript:run_suite();")) 1494 1495 if edit: 1496 # Find the directory path containing this suite. 1497 (dirname, basename) = self.GetDatabase().SplitLabel(suite.GetId()) 1498 1499 # Construct a list of all test IDs, relative to the suite, 1500 # that are not explicitly included in the suite. 1501 excluded_test_ids = database.GetTestIds(dirname) 1502 for test_id in self.test_ids: 1503 if test_id in excluded_test_ids: 1504 excluded_test_ids.remove(test_id) 1505 # Make controls for adding or removing test IDs. 1506 self.test_id_controls = qm.web.make_choose_control( 1507 "test_ids", 1508 "Included Tests", 1509 self.test_ids, 1510 "Available Tests", 1511 excluded_test_ids) 1512 1513 # Likewise for suite IDs. 1514 excluded_suite_ids = database.GetSuiteIds(dirname) 1515 for suite_id in self.suite_ids: 1516 if suite_id in excluded_suite_ids: 1517 excluded_suite_ids.remove(suite_id) 1518 # Don't show the suite as a candidate for inclusion in 1519 # itself. 1520 self_suite_id = basename 1521 if self_suite_id in excluded_suite_ids: 1522 excluded_suite_ids.remove(self_suite_id) 1523 # Make controls for adding or removing suite IDs. 1524 self.suite_id_controls = qm.web.make_choose_control( 1525 "suite_ids", 1526 "Included Suites", 1527 self.suite_ids, 1528 "Available Suites", 1529 excluded_suite_ids)
1530 1531
1532 - def MakeEditUrl(self):
1533 """Return the URL for editing this suite.""" 1534 1535 return qm.web.WebRequest("edit-suite", 1536 base=self.request, 1537 id=self.suite.GetId()) \ 1538 .AsUrl()
1539 1540
1541 - def MakeRunUrl(self):
1542 """Return the URL for running this suite.""" 1543 1544 return qm.web.WebRequest("run-tests", 1545 base=self.request, 1546 ids=self.suite.GetId()) \ 1547 .AsUrl()
1548 1549
1550 - def MakeDeleteScript(self):
1551 """Make a script to confirm deletion of the suite. 1552 1553 returns -- JavaScript source for a function, 'delete_script', 1554 which shows a popup confirmation window.""" 1555 1556 suite_id = self.suite.GetId() 1557 delete_url = qm.web.make_url("delete-suite", 1558 base_request=self.request, 1559 id=suite_id) 1560 message = """ 1561 <p>Are you sure you want to delete the suite %s?</p> 1562 """ % suite_id 1563 return self.server.MakeConfirmationDialog(message, delete_url)
1564 1565 1566
1567 -class StorageResultsStream(ResultStream):
1568 """A 'StorageResultsStream' stores results. 1569 1570 A 'StorageResultsStream' does not write any output. It simply 1571 stores the results for future display.""" 1572
1573 - def __init__(self):
1574 """Construct a 'StorageResultsStream'.""" 1575 1576 super(StorageResultsStream, self).__init__({}) 1577 self.__test_results = {} 1578 self.__test_results_in_order = [] 1579 self.__resource_results = {} 1580 # The stream is not finished yet. 1581 self.__is_finished = 0 1582 # And there are no annotations yet. 1583 self.__annotations = {} 1584 1585 # Create a lock for synchronization between the test execution 1586 # thread (which will call methods like 'WriteResults' and 1587 # 'Summarize') and the GUI thread (which will call 1588 # 'GetTestResults' and 'IsFinished'). 1589 self.__lock = Lock()
1590 1591
1592 - def GetAnnotations(self):
1593 """Return the annotations for this run.""" 1594 1595 return self.__annotations
1596 1597
1598 - def WriteAnnotation(self, key, value):
1599 1600 self.__annotations[key] = value
1601 1602
1603 - def WriteResult(self, result):
1604 """Output a test result. 1605 1606 'result' -- A 'Result'.""" 1607 1608 self.__lock.acquire() 1609 try: 1610 if result.GetKind() == Result.TEST: 1611 self.__test_results[result.GetId()] = result 1612 self.__test_results_in_order.append(result) 1613 else: 1614 self.__resource_results[result.GetId()] = result 1615 finally: 1616 self.__lock.release()
1617 1618
1619 - def Summarize(self):
1620 """Output summary information about the results. 1621 1622 When this method is called, the test run is complete. Summary 1623 information should be displayed for the user, if appropriate. 1624 Any finalization, such as the closing of open files, should 1625 also be performed at this point. 1626 1627 Derived class methods may override this method. They should, 1628 however, invoke this version before returning.""" 1629 1630 # Mark the stream as finished. 1631 self.__lock.acquire() 1632 ResultStream.Summarize(self) 1633 self.__is_finished = 1 1634 self.__lock.release()
1635 1636
1637 - def Start(self, test_ids):
1638 """Start collecting results. 1639 1640 'test_ids' -- The names of the tests that we are about to run. 1641 1642 Start collecting new results. Discard results for the 1643 'test_ids', but not for other tests.""" 1644 1645 self.__lock.acquire() 1646 self.__is_finished = 0 1647 # Go through all of the tests we are about to run and remove 1648 # corresponding results. 1649 for id in test_ids: 1650 if self.__test_results.has_key(id): 1651 del self.__test_results[id] 1652 self.__test_results_in_order \ 1653 = filter(lambda r, rs=self.__test_results: \ 1654 rs.has_key(r.GetId()), 1655 self.__test_results_in_order) 1656 self.__lock.release()
1657 1658
1659 - def IsFinished(self):
1660 """Return true iff no more results are forthcoming. 1661 1662 returns -- True if no more results will be written to this 1663 stream.""" 1664 1665 self.__lock.acquire() 1666 finished = self.__is_finished 1667 self.__lock.release() 1668 return finished
1669 1670
1671 - def GetTestResults(self):
1672 """Return the accumulated test results. 1673 1674 returns -- A dictionary mapping test names to 'Result' objects.""" 1675 1676 self.__lock.acquire() 1677 results = self.__test_results 1678 self.__lock.release() 1679 return results
1680 1681
1682 - def GetTestResultsInOrder(self):
1683 """Return the test results in the order they appeared. 1684 1685 returns -- A sequence of test results, in the order that they 1686 appeared.""" 1687 1688 self.__lock.acquire() 1689 results = self.__test_results_in_order 1690 self.__lock.release() 1691 return results
1692 1693
1694 - def GetResourceResults(self):
1695 """Return the accumulated resource results. 1696 1697 returns -- A dictionary mapping resource names to 'Result' 1698 objects.""" 1699 1700 self.__lock.acquire() 1701 results = self.__resource_results 1702 self.__lock.release() 1703 return results
1704 1705
1706 - def GetResult(self, name):
1707 """Return the 'Result' with the indicated 'name'. 1708 1709 'name' -- A string giving the name of a test or resource result. 1710 1711 returns -- The 'Result' instance corresponding to 'name'.""" 1712 1713 self.__lock.acquire() 1714 result = self.__test_results.get(name) 1715 if not result: 1716 result = self.__resource_results.get(name) 1717 self.__lock.release() 1718 1719 return result
1720 1721
1722 -class TestResultsPage(QMTestPage):
1723 """DTML page for displaying test results.""" 1724
1725 - def __init__(self, server):
1726 """Construct a new 'TestResultsPage'. 1727 1728 'server' -- The 'QMTestServer' creating this page.""" 1729 1730 # Initialize the base classes. 1731 QMTestPage.__init__(self, "results.dtml", server) 1732 1733 results_stream = server.GetResultsStream() 1734 # It is important that we ask for IsFinished before asking 1735 # for GetTestResults. The stream could be finished between 1736 # the two calls, and it is better to show all the results but 1737 # claim they are incomplete than to show only some of the 1738 # results and claim they are complete. 1739 self.__is_finished = results_stream.IsFinished() 1740 self.test_results = results_stream.GetTestResultsInOrder() 1741 self.expected_outcomes = server.GetExpectedOutcomes()
1742 1743
1744 - def GetOutcomes(self):
1745 """Return the list of result outcomes. 1746 1747 returns -- A sequence of result outcomes.""" 1748 1749 return Result.outcomes
1750 1751
1752 - def GetTotal(self):
1753 """Return the total number of tests. 1754 1755 returns -- The total number of tests.""" 1756 1757 return len(self.test_results)
1758 1759
1760 - def GetTotalUnexpected(self):
1761 """Return the total number of unexpected results. 1762 1763 returns -- The total number of unexpected results.""" 1764 1765 return len(self.GetRelativeResults(self.test_results, 0))
1766 1767
1768 - def GetResultsWithOutcome(self, outcome):
1769 """Return the number of tests with the given 'outcome'. 1770 1771 'outcome' -- One of the 'Result.outcomes'. 1772 1773 returns -- The results with the given 'outcome'.""" 1774 1775 return filter(lambda r, o=outcome: r.GetOutcome() == o, 1776 self.test_results)
1777 1778
1779 - def GetCount(self, outcome):
1780 """Return the number of tests with the given 'outcome'. 1781 1782 'outcome' -- One of the 'Result.outcomes'. 1783 1784 returns -- The number of tests with the given 'outcome'.""" 1785 1786 return len(self.GetResultsWithOutcome(outcome))
1787 1788
1789 - def GetUnexpectedCount(self, outcome):
1790 """Return the number of tests with the given 'outcome'. 1791 1792 'outcome' -- One of the 'Result.outcomes'. 1793 1794 returns -- The number of tests with the given 'outcome' that 1795 were expected to have some other outcome.""" 1796 1797 results = self.GetResultsWithOutcome(outcome) 1798 results = self.GetRelativeResults(results, 0) 1799 return len(results)
1800 1801
1802 - def GetRelativeResults(self, results, expected):
1803 """Return the results that match, or fail to match, expectations. 1804 1805 'results' -- A sequence of 'Result' objects. 1806 1807 'expected' -- A boolean. If true, expected results are 1808 returned. If false, unexpected results are returned.""" 1809 1810 if expected: 1811 return filter(lambda r, er=self.expected_outcomes: \ 1812 r.GetOutcome() == er.get(r.GetId(), 1813 Result.PASS), 1814 results) 1815 else: 1816 return filter(lambda r, er=self.expected_outcomes: \ 1817 r.GetOutcome() != er.get(r.GetId(), 1818 Result.PASS), 1819 results)
1820 1821
1822 - def GetDetailUrl(self, test_id):
1823 """Return the detail URL for a test. 1824 1825 'test_id' -- The name of the test. 1826 1827 returns -- The URL that contains details about the 'test_id'.""" 1828 1829 return qm.web.WebRequest("show-result", 1830 base=self.request, 1831 id=test_id).AsUrl()
1832 1833
1834 - def IsFinished(self):
1835 """Return true iff no more results are forthcoming. 1836 1837 returns -- True if no more tests are running.""" 1838 1839 return self.__is_finished
1840 1841
1842 - def GetRefreshDelay(self):
1843 """Returns the number of seconds to wait before refreshing the page. 1844 1845 returns -- The number of seconds to wait before refreshing this 1846 page. A value of zero means that te page should never be 1847 refreshed. This function is only called if 'IsFinished' returns 1848 true.""" 1849 1850 return 10
1851 1852 1853
1854 -class QMTestServer(qm.web.WebServer):
1855 """A 'QMTestServer' is the web GUI interface to QMTest.""" 1856
1857 - def __init__(self, database, port, address, log_file, 1858 targets, context, expectations, 1859 run_db):
1860 """Create and bind an HTTP server. 1861 1862 'database' -- The test database to serve. 1863 1864 'port' -- The port number on which to accept HTTP requests. 1865 1866 'address' -- The local address to which to bind the server. An 1867 empty string indicates all local addresses. 1868 1869 'log_file' -- A file object to which the server will log requests. 1870 'None' for no logging. 1871 1872 'targets' -- A sequence of 'Target' objects to use when running 1873 tests. 1874 1875 'context' -- The 'Context' in which tests will execute.""" 1876 1877 qm.web.WebServer.__init__(self, port, address, log_file=log_file) 1878 1879 self.__database = database 1880 self.__targets = targets 1881 self.__context = context 1882 1883 # Register all our web pages. 1884 for name, function in [ 1885 ( "/test/clear-results", self.HandleClearResults ), 1886 ( "/test/create-resource", self.HandleShowItem ), 1887 ( "/test/create-suite", self.HandleCreateSuite ), 1888 ( "/test/create-test", self.HandleShowItem ), 1889 ( "/test/delete-resource", self.HandleDeleteItem ), 1890 ( "/test/delete-suite", self.HandleDeleteSuite ), 1891 ( "/test/delete-test", self.HandleDeleteItem ), 1892 ( "/test/dir", self.HandleDir ), 1893 ( "/test/edit-context", self.HandleEditContext ), 1894 ( "/test/edit-resource", self.HandleShowItem ), 1895 ( "/test/edit-suite", self.HandleEditSuite ), 1896 ( "/test/edit-test", self.HandleShowItem ), 1897 ( "/test/load-context", self.HandleLoadContext ), 1898 ( "/test/load-expectations", self.HandleLoadExpectations ), 1899 ( "/test/load-results", self.HandleLoadResults ), 1900 ( "/test/new-resource", self.HandleNewResource ), 1901 ( "/test/new-suite", self.HandleNewSuite ), 1902 ( "/test/new-test", self.HandleNewTest ), 1903 ( "/test/run-tests", self.HandleRunTests ), 1904 ( "/test/set-expectation", self.HandleSetExpectation ), 1905 ( "/test/show-dir", self.HandleDir ), 1906 ( "/test/show-resource", self.HandleShowItem ), 1907 ( "/test/show-result", self.HandleShowResult ), 1908 ( "/test/show-results", self.HandleShowResults ), 1909 ( "/test/show-suite", self.HandleShowSuite ), 1910 ( "/test/show-test", self.HandleShowItem ), 1911 ( "/test/shutdown", self.HandleShutdown ), 1912 ( "/test/stop-tests", self.HandleStopTests ), 1913 ( "/test/submit-context", self.HandleSubmitContext ), 1914 ( "/test/submit-context-file", self.HandleSubmitContextFile ), 1915 ( "/test/submit-expectation", self.HandleSubmitExpectation ), 1916 ( "/test/submit-resource", self.HandleSubmitItem ), 1917 ( "/test/submit-expectations", self.HandleSubmitExpectations ), 1918 ( "/test/submit-expectations-form", self.HandleSubmitExpectationsForm ), 1919 ( "/test/submit-results", self.HandleSubmitResults ), 1920 ( "/test/submit-suite", self.HandleSubmitSuite ), 1921 ( "/test/submit-test", self.HandleSubmitItem ), 1922 ( "/test/" + qm.test.cmdline.QMTest.context_file_name, 1923 self.HandleSaveContext ), 1924 ( "/test/" + qm.test.cmdline.QMTest.expectations_file_name, 1925 self.HandleSaveExpectations ), 1926 ( "/test/" + qm.test.cmdline.QMTest.results_file_name, 1927 self.HandleSaveResults ), 1928 ( "/report/dir", self.HandleDirReport ), 1929 ( "/report/show-dir", self.HandleDirReport ), 1930 ( "/report/show-test", self.HandleShowItemReport ), 1931 ( "/report/show-resource", self.HandleShowItemReport ), 1932 ( "/report/show-result", self.HandleShowResultReport ), 1933 ( "/report/show-result", self.HandleShowResult ), 1934 ]: 1935 self.RegisterScript(name, function) 1936 1937 1938 self.RegisterPathTranslation( 1939 "/stylesheets", qm.get_share_directory("web", "stylesheets")) 1940 self.RegisterPathTranslation( 1941 "/images", qm.get_share_directory("web", "images")) 1942 self.RegisterPathTranslation( 1943 "/static", qm.get_share_directory("web", "static")) 1944 # Register the QM manual. 1945 self.RegisterPathTranslation( 1946 "/tutorial", qm.get_doc_directory("html", "tutorial")) 1947 1948 # The DB's attachment store processes download requests for 1949 # attachment data. 1950 attachment_store = database.GetAttachmentStore() 1951 if attachment_store: 1952 self.RegisterScript(qm.fields.AttachmentField.download_url, 1953 attachment_store.HandleDownloadRequest) 1954 1955 self.__expectation_db = expectations 1956 self.__expected_outcomes = {} 1957 for test_id in self.__database.GetTestIds(): 1958 result = expectations.Lookup(test_id) 1959 self.__expected_outcomes[test_id] = result.GetOutcome() 1960 self.__run_db = run_db 1961 # There are no results yet. 1962 self.__results_stream = StorageResultsStream() 1963 self.__results_stream.Summarize() 1964 # There is no execution thread. 1965 self.__execution_thread = None 1966 1967 # Bind the server to the specified address. 1968 try: 1969 self.Bind() 1970 except qm.web.AddressInUseError, address: 1971 raise RuntimeError, qm.error("address in use", address=address) 1972 except qm.web.PrivilegedPortError: 1973 raise RuntimeError, qm.error("privileged port", port=port)
1974 1975
1976 - def GetContext(self):
1977 """Return the 'Context' in which tests will be run. 1978 1979 returns -- The 'Context' in which tests will be run.""" 1980 1981 return self.__context
1982 1983
1984 - def GetDatabase(self):
1985 """Return the 'Database' handled by this server. 1986 1987 returns -- The 'Database' handled by this server.""" 1988 1989 return self.__database
1990 1991
1992 - def GetRunDatabase(self):
1993 """Return the 'RunDatabase' handled by this server. 1994 1995 returns -- The 'RunDatabase' handled by this server.""" 1996 1997 return self.__run_db
1998 1999
2000 - def GetExpectationDatabase(self):
2001 """Return the current ExpectationDatabase. 2002 2003 returns -- The ExpectationDatabase instance.""" 2004 2005 return self.__expectation_db
2006 2007
2008 - def GetExpectedOutcomes(self):
2009 """Return the current expected outcomes for the test database. 2010 2011 returns -- A map from test IDs to outcomes. Some tests may have 2012 not have an entry in the map.""" 2013 2014 return self.__expected_outcomes
2015 2016
2017 - def GetHTMLClassForOutcome(self, outcome):
2018 """Return the CSS class for the 'outcome'. 2019 2020 'outcome' -- One of the result outcomes. 2021 2022 returns -- The name of a CSS class. These are used with <span> 2023 elements. See 'qm.css'.""" 2024 2025 return { 2026 Result.PASS: "qmtest_pass", 2027 Result.FAIL: "qmtest_fail", 2028 Result.UNTESTED: "qmtest_untested", 2029 Result.ERROR: "qmtest_error", 2030 "EXPECTED" : "qmtest_expected" 2031 }[outcome]
2032 2033
2034 - def GetResultsStream(self):
2035 """Return the 'StorageResultsStream' containing test results. 2036 2037 returns -- The 'StorageResultsStream' associated with this 2038 server.""" 2039 2040 return self.__results_stream
2041 2042
2043 - def HandleClearResults(self, request):
2044 """Handle a request to clear the current test results. 2045 2046 'request' -- A 'WebRequest' object.""" 2047 2048 # Eliminate the old results stream. 2049 del self.__results_stream 2050 # And create a new one. 2051 self.__results_stream = StorageResultsStream() 2052 self.__results_stream.Summarize() 2053 2054 # Redirect to the main page. 2055 request = qm.web.WebRequest("dir", base=request) 2056 raise qm.web.HttpRedirect, request
2057 2058
2059 - def HandleCreateSuite(self, request):
2060 """Handle a submission of a new test suite. 2061 2062 'request' -- A 'WebRequest' object.""" 2063 2064 field_errors = {} 2065 database = self.__database 2066 2067 # Extract the suite ID of the new suite from the request. 2068 suite_id = request["id"] 2069 # Check that the ID is valid. 2070 if not database.IsValidLabel(suite_id, is_component = 0): 2071 field_errors["_id"] = qm.error("invalid id", id=suite_id) 2072 # Check that the ID doesn't already exist. 2073 elif database.HasSuite(suite_id): 2074 field_errors["_id"] = qm.error("suite already exists", 2075 suite_id=suite_id) 2076 2077 # Were there any validation errors? 2078 if len(field_errors) > 0: 2079 # Yes. Instead of showing the page for editing the suite, 2080 # redisplay the new suite page with error messages. 2081 return NewSuitePage(self, suite_id, field_errors)(request) 2082 else: 2083 # Everything looks good. Make an empty suite. 2084 suite_class = qm.test.base.get_extension_class( 2085 "explicit_suite.ExplicitSuite", 2086 "suite", 2087 self.GetDatabase()) 2088 extras = { suite_class.EXTRA_DATABASE : self.GetDatabase(), 2089 suite_class.EXTRA_ID : suite_id } 2090 suite = suite_class({}, **extras) 2091 # Show the editing page. 2092 return ShowSuitePage(self, suite, edit=1, is_new_suite=1)(request)
2093 2094
2095 - def HandleDeleteItem(self, request):
2096 """Handle a request to delete a test or resource. 2097 2098 This function handles the script requests 'delete-test' and 2099 'delete-resource'. 2100 2101 'request' -- A 'WebRequest' object. 2102 2103 The ID of the test or resource to delete is specified in the 'id' 2104 field of the request.""" 2105 2106 database = self.__database 2107 # Extract the item ID. 2108 item_id = request["id"] 2109 # The script name determines whether we're deleting a test or an 2110 # resource. 2111 script_name = request.GetScriptName() 2112 if script_name == "delete-test": 2113 database.RemoveExtension(item_id, database.TEST) 2114 elif script_name == "delete-resource": 2115 database.RemoveExtension(item_id, database.RESOURCE) 2116 else: 2117 raise RuntimeError, "unrecognized script name" 2118 # Redirect to the main page. 2119 request = qm.web.WebRequest("dir", base=request) 2120 raise qm.web.HttpRedirect, request
2121 2122
2123 - def HandleDeleteSuite(self, request):
2124 """Handle a request to delete a test suite. 2125 2126 'request' -- A 'WebRequest' object. 2127 2128 The ID of the suite to delete is specified in the 'id' field of the 2129 request.""" 2130 2131 database = self.__database 2132 # Extract the suite ID. 2133 suite_id = request["id"] 2134 database.RemoveExtension(suite_id, database.SUITE) 2135 # Redirect to the main page. 2136 raise qm.web.HttpRedirect, qm.web.WebRequest("dir", base=request)
2137 2138
2139 - def HandleDir(self, request):
2140 """Generate a directory page. 2141 2142 'request' -- A 'WebRequest' object. 2143 2144 The request has these fields: 2145 2146 'path' -- A path in test/resource/suite ID space. If specified, 2147 only tests and resources in this subtree are displayed, and their 2148 IDs are displayed relative to this path. If omitted, the entire 2149 contents of the test database are shown.""" 2150 2151 path = request.get("id", "") 2152 return DirPage(self, path)(request)
2153 2154
2155 - def HandleDirReport(self, request):
2156 """Generate a directory report page. 2157 2158 'request' -- A 'WebRequest' object. 2159 2160 The request has these fields: 2161 2162 'path' -- A path in test/resource/suite ID space. If specified, 2163 only tests and resources in this subtree are displayed, and their 2164 IDs are displayed relative to this path. If omitted, the entire 2165 contents of the test database are shown.""" 2166 2167 path = request.get("id", "") 2168 return DirReportPage(self, path)(request)
2169 2170
2171 - def HandleEditContext(self, request):
2172 """Handle a request to edit the context. 2173 2174 'request' -- The 'WebRequest' that caused the event.""" 2175 2176 context_page = ContextPage(self) 2177 return context_page(request)
2178 2179
2180 - def HandleEditSuite(self, request):
2181 """Generate the page for editing a test suite.""" 2182 2183 return self.HandleShowSuite(request, edit=1)
2184 2185
2186 - def HandleLoadContext(self, request):
2187 """Handle a request to upload a context file. 2188 2189 'request' -- The 'WebRequest' that caused the event.""" 2190 2191 return LoadContextPage(self)(request)
2192 2193
2194 - def HandleLoadExpectations(self, request):
2195 """Handle a request to upload results. 2196 2197 'request' -- The 'WebRequest' that caused the event.""" 2198 2199 return LoadExpectationsPage(self)(request)
2200 2201
2202 - def HandleLoadResults(self, request):
2203 """Handle a request to upload results. 2204 2205 'request' -- The 'WebRequest' that caused the event.""" 2206 2207 return LoadResultsPage(self)(request)
2208 2209
2210 - def HandleNewResource(self, request):
2211 """Handle a request to create a new test. 2212 2213 'request' -- The 'WebRequest' that caused the event.""" 2214 2215 return NewItemPage(self, "resource")(request)
2216 2217
2218 - def HandleNewTest(self, request):
2219 """Handle a request to create a new test. 2220 2221 'request' -- The 'WebRequest' that caused the event.""" 2222 2223 return NewItemPage(self, "test")(request)
2224 2225
2226 - def HandleNewSuite(self, request):
2227 """Handle a request to create a new suite. 2228 2229 'request' -- The 'WebRequest' that caused the event.""" 2230 2231 return NewSuitePage(self)(request)
2232 2233
2234 - def HandleRunTests(self, request):
2235 """Handle a request to run tests. 2236 2237 'request' -- The 'WebRequest' that caused the event. 2238 2239 These fields in 'request' are used: 2240 2241 'ids' -- A comma-separated list of test and suite IDs. These IDs 2242 are expanded into the list of IDs of tests to run. 2243 2244 """ 2245 2246 # Extract and expand the IDs of tests to run. 2247 if request.has_key("ids"): 2248 ids = string.split(request["ids"], ",") 2249 # '.' is an alias for <all>, and thus shadows other selectors. 2250 if '.' in ids: 2251 ids = [""] 2252 else: 2253 ids = [""] 2254 test_ids = self.GetDatabase().ExpandIds(ids)[0] 2255 2256 # Let the results stream know that we are going to start 2257 # providing it with results. 2258 self.__results_stream.Start(test_ids) 2259 2260 # Create the thread that will run all of the tests. 2261 del self.__execution_thread 2262 test_ids.sort() 2263 self.__execution_thread = \ 2264 ExecutionThread(self.__database, test_ids, self.__context, 2265 self.__targets, [self.__results_stream], 2266 self.__expectation_db) 2267 # Start the thread. 2268 self.__execution_thread.start() 2269 2270 # Sleep for a few seconds so that if we're only running one 2271 # test there's a good chance that it will finish before we 2272 # show the results page. 2273 time.sleep(5) 2274 2275 # Redirect to the results page. 2276 request = qm.web.WebRequest("show-results", base=request) 2277 raise qm.web.HttpRedirect, request
2278 2279
2280 - def HandleSaveContext(self, request):
2281 """Handlea request to save the context to a file. 2282 2283 'request' -- The 'WebRequest' that caused the event.""" 2284 2285 # Start with the empty string. 2286 s = "" 2287 # Run through all of the context variables. 2288 for (name, value) in self.__context.items(): 2289 s = s + "%s=%s\n" % (name, value) 2290 2291 return ("application/x-qmtest-context", s)
2292 2293
2294 - def HandleSaveExpectations(self, request):
2295 """Handle a request to save expectations to a file. 2296 2297 'request' -- The 'WebRequest' that caused the event.""" 2298 2299 # Create a string stream to store the results. 2300 s = StringIO.StringIO() 2301 # Create a results stream for storing the results. 2302 rsc = qm.test.cmdline.get_qmtest().GetFileResultStreamClass() 2303 rs = rsc({ "file" : s }) 2304 # Write all the results. 2305 for (id, outcome) in self.__expected_outcomes.items(): 2306 r = Result(Result.TEST, id, outcome) 2307 rs.WriteResult(r) 2308 # Terminate the stream. 2309 rs.Summarize() 2310 # Extract the data. 2311 data = s.getvalue() 2312 # Close the stream. 2313 s.close() 2314 2315 return ("application/x-qmtest-results", data)
2316 2317
2318 - def HandleSaveResults(self, request):
2319 """Handle a request to save results to a file. 2320 2321 'request' -- The 'WebRequest' that caused the event.""" 2322 2323 # Create a string stream to store the results. 2324 s = StringIO.StringIO() 2325 # Create a results stream for storing the results. 2326 rsc = qm.test.cmdline.get_qmtest().GetFileResultStreamClass() 2327 rs = rsc({ "file" : s }) 2328 # Write all the annotations. 2329 rs.WriteAllAnnotations(self.__results_stream.GetAnnotations()) 2330 # Write all the results. 2331 for r in self.__results_stream.GetTestResults().values(): 2332 rs.WriteResult(r) 2333 for r in self.__results_stream.GetResourceResults().values(): 2334 rs.WriteResult(r) 2335 # Terminate the stream. 2336 rs.Summarize() 2337 # Extract the data. 2338 data = s.getvalue() 2339 # Close the stream. 2340 s.close() 2341 2342 return ("application/x-qmtest-results", data)
2343 2344
2345 - def HandleSetExpectation(self, request):
2346 """Handle a request to set expectations. 2347 2348 'request' -- A 'WebRequest' object.""" 2349 2350 return SetExpectationPage(self, request["id"])(request)
2351 2352
2353 - def HandleShowItem(self, request):
2354 """Handle a request to show a test or resource. 2355 2356 'request' -- A 'WebRequest' object. 2357 2358 This function generates pages to handle these requests: 2359 2360 'create-test' -- Generate a form for initial editing of a test 2361 about to be created, given its test ID and test class. 2362 2363 'create-resource' -- Likewise for an resource. 2364 2365 'show-test' -- Display a test. 2366 2367 'show-resource' -- Likewise for an resource. 2368 2369 'edit-test' -- Generate a form for editing an existing test. 2370 2371 'edit-resource' -- Likewise for an resource. 2372 2373 This function distinguishes among these cases by checking the script 2374 name of the request object. 2375 2376 The request must have the following fields: 2377 2378 'id' -- A test or resource ID. For show or edit pages, the ID of an 2379 existing item. For create pages, the ID of the item being 2380 created. 2381 2382 'class' -- For create pages, the name of the test or resource 2383 class. 2384 2385 """ 2386 2387 # Paramaterize this function based on the request's script name. 2388 url = request.GetScriptName() 2389 edit, create, type = { 2390 "show-test": (0, 0, "test"), 2391 "edit-test": (1, 0, "test"), 2392 "create-test": (1, 1, "test"), 2393 "show-resource": (0, 0, "resource"), 2394 "edit-resource": (1, 0, "resource"), 2395 "create-resource": (1, 1, "resource"), 2396 }[url] 2397 2398 database = self.__database 2399 2400 try: 2401 # Determine the ID of the item. 2402 item_id = request["id"] 2403 except KeyError: 2404 # The user probably submitted the form without entering an ID. 2405 message = qm.error("no id for show") 2406 return qm.web.generate_error_page(request, message) 2407 2408 if create: 2409 # We're in the middle of creating a new item. 2410 class_name = request["class"] 2411 2412 # First perform some validation. 2413 field_errors = {} 2414 # Check that the ID is valid. 2415 if not database.IsValidLabel(item_id, is_component = 0): 2416 field_errors["_id"] = qm.error("invalid id", id=item_id) 2417 else: 2418 # Check that the ID doesn't already exist. 2419 if type == "resource": 2420 if database.HasResource(item_id): 2421 field_errors["_id"] \ 2422 = qm.error("resource already exists", 2423 resource_id=item_id) 2424 elif type == "test": 2425 if database.HasTest(item_id): 2426 field_errors["_id"] = qm.error("test already exists", 2427 test_id=item_id) 2428 # Check that the class exists. 2429 try: 2430 qm.test.base.get_extension_class(class_name, type, 2431 database) 2432 except ValueError: 2433 # The class name was incorrectly specified. 2434 field_errors["_class"] = qm.error("invalid class name", 2435 class_name=class_name) 2436 except: 2437 # Can't find the class. 2438 field_errors["_class"] = qm.error("class not found", 2439 class_name=class_name) 2440 # Were there any errors? 2441 if len(field_errors) > 0: 2442 # Yes. Instead of showing the edit page, re-show the new 2443 # item page. 2444 page = NewItemPage(server=self, 2445 type=type, 2446 item_id=item_id, 2447 class_name=class_name, 2448 field_errors=field_errors) 2449 return page(request) 2450 2451 # Construct a test with default argument values, as the 2452 # starting point for editing. 2453 if type == "resource": 2454 item = self.MakeNewResource(class_name, item_id) 2455 elif type == "test": 2456 item = self.MakeNewTest(class_name, item_id) 2457 else: 2458 # We're showing or editing an existing item. 2459 # Look it up in the database. 2460 if type == "resource": 2461 try: 2462 item = database.GetResource(item_id) 2463 except qm.test.database.NoSuchTestError, e: 2464 # An test with the specified test ID was not fount. 2465 # Show a page indicating the error. 2466 return qm.web.generate_error_page(request, str(e)) 2467 elif type == "test": 2468 try: 2469 item = database.GetTest(item_id) 2470 except qm.test.database.NoSuchResourceError, e: 2471 # An test with the specified resource ID was not fount. 2472 # Show a page indicating the error. 2473 return qm.web.generate_error_page(request, str(e)) 2474 2475 # Generate HTML. 2476 return ShowItemPage(self, item, edit, create, type)(request)
2477 2478
2479 - def HandleShowResult(self, request):
2480 """Handle a request to show result detail. 2481 If a 'test_run' argument was provided, fetch the result from the 2482 corresponding test run. Else read it from the results stream. 2483 2484 'request' -- The 'WebRequest' that caused the event.""" 2485 2486 name = request["id"] 2487 test_run = request.get("test_run") 2488 if test_run: 2489 run_db = self.GetRunDatabase() 2490 result = run_db.GetAllRuns()[int(test_run)].GetResult(name) 2491 else: 2492 result = self.__results_stream.GetResult(name) 2493 return ResultPage(self, result)(request)
2494 2495
2496 - def HandleShowResults(self, request):
2497 """Handle a request to show results. 2498 2499 'request' -- The 'WebRequest' that caused the event.""" 2500 2501 # Display the results. 2502 results_page = TestResultsPage(self) 2503 return results_page(request)
2504 2505
2506 - def HandleShowItemReport(self, request):
2507 """Handle a request to show a test or resource report. 2508 2509 'request' -- A 'WebRequest' object. 2510 2511 This function generates pages to handle these requests: 2512 2513 'show-test' -- Display a test. 2514 2515 'show-resource' -- Likewise for an resource. 2516 2517 This function distinguishes among these cases by checking the script 2518 name of the request object. 2519 2520 The request must have the following fields: 2521 2522 'id' -- A test or resource ID. For show or edit pages, the ID of an 2523 existing item. For create pages, the ID of the item being 2524 created.""" 2525 2526 # Paramaterize this function based on the request's script name. 2527 url = request.GetScriptName() 2528 type = {"show-test": "test", 2529 "show-resource": "resource"}[url] 2530 2531 database = self.GetDatabase() 2532 2533 try: 2534 # Determine the ID of the item. 2535 item_id = request["id"] 2536 except KeyError: 2537 # The user probably submitted the form without entering an ID. 2538 message = qm.error("no id for show") 2539 return qm.web.generate_error_page(request, message) 2540 2541 if type == "resource": 2542 try: 2543 item = database.GetResource(item_id) 2544 except qm.test.database.NoSuchTestError, e: 2545 return qm.web.generate_error_page(request, str(e)) 2546 elif type == "test": 2547 try: 2548 item = database.GetTest(item_id) 2549 except qm.test.database.NoSuchResourceError, e: 2550 return qm.web.generate_error_page(request, str(e)) 2551 2552 # Generate HTML. 2553 return ShowItemReportPage(self, item, type)(request)
2554 2555
2556 - def HandleShowResultReport(self, request):
2557 """Handle a request to show result report. 2558 2559 'request' -- The 'WebRequest' that caused the event.""" 2560 2561 return xxx
2562 #name = request["id"] 2563 #result = self.__results_stream.GetResult(name) 2564 #return ResultPage(self, result)(request) 2565 2566
2567 - def HandleShowSuite(self, request, edit=0):
2568 """Generate the page for displaying or editing a test suite. 2569 2570 'request' -- A 'WebRequest' object. 2571 2572 'edit' -- If true, display the page for editing the suite. 2573 Otherwise, just display the suite. 2574 2575 The request has the following fields: 2576 2577 'id' -- The ID of the suite to display or edit.""" 2578 2579 database = self.__database 2580 2581 try: 2582 # Determine the suite ID. 2583 suite_id = request["id"] 2584 except KeyError: 2585 # No suite ID was given. 2586 message = qm.error("no id for show") 2587 return qm.web.generate_error_page(request, message) 2588 else: 2589 suite = database.GetSuite(suite_id) 2590 # Generate HTML. 2591 return ShowSuitePage(self, suite, edit, is_new_suite=0)(request)
2592 2593
2594 - def HandleShutdown(self, request):
2595 """Handle a request to shut down the server. 2596 2597 'request' -- The 'WebRequest' that caused the event.""" 2598 2599 raise SystemExit, None
2600 2601
2602 - def HandleStopTests(self, request):
2603 """Handle a request to stop test execution. 2604 2605 'request' -- The 'WebRequest' that caused the event.""" 2606 2607 # Stop the thread. 2608 self.__execution_thread.RequestTermination() 2609 # Redirect to the results page. 2610 request = qm.web.WebRequest("show-results", base=request) 2611 raise qm.web.HttpRedirect, request
2612 2613
2614 - def HandleSubmitContext(self, request):
2615 """Handle a context submission.. 2616 2617 'request' -- The 'WebRequest' that caused the event. The 2618 'request' must have a 'context_vars' key, whose value is the 2619 the context variables.""" 2620 2621 vars = qm.web.decode_properties(request["context_vars"]) 2622 self.__context = Context() 2623 for k in vars.keys(): 2624 self.__context[k] = vars[k] 2625 2626 # Redirect to the main page. 2627 request = qm.web.WebRequest("dir", base=request) 2628 raise qm.web.HttpRedirect, request
2629 2630
2631 - def HandleSubmitContextFile(self, request):
2632 """Handle a context file submission.. 2633 2634 'request' -- The 'WebRequest' that caused the event.""" 2635 2636 # The context data. 2637 data = request["file"] 2638 # Create a file objet to read from. 2639 file = StringIO.StringIO(data) 2640 # Parse the assignments in the context file. 2641 assignments = qm.common.read_assignments(file) 2642 # Add them to the context. 2643 for (name, value) in assignments.items(): 2644 try: 2645 self.__context[name] = value 2646 except ValueError: 2647 # Skip any invalid assignments. 2648 pass 2649 # Redirect to the main page. 2650 return self._ClosePopupAndRedirect("dir")
2651 2652
2653 - def HandleSubmitExpectation(self, request):
2654 """Handle setting a single expectation. 2655 2656 'request' -- The 'WebRequest' that caused the event.""" 2657 2658 id = request["id"] 2659 outcome = request["outcome"] 2660 self.__expected_outcomes[id] = outcome 2661 # Close the upload popup window, and reload the main window. 2662 return self._ClosePopupAndRedirect(request["url"])
2663 2664
2665 - def HandleSubmitExpectations(self, request):
2666 """Handle uploading expected results. 2667 2668 'request' -- The 'WebRequest' that caused the event.""" 2669 2670 # Get the results file data. 2671 data = request["file"] 2672 # Create a file object from the data. 2673 f = StringIO.StringIO(data) 2674 # Read the results. 2675 self.__expectation_db = \ 2676 qm.test.base.load_expectations(f, self.GetDatabase()) 2677 self.__expected_outcomes = {} 2678 for test_id in self.GetDatabase().GetTestIds(): 2679 expectation = self.__expectation_db.Lookup(test_id) 2680 self.__expected_outcomes[test_id] = expectation.GetOutcome() 2681 # Close the upload popup window, and redirect the main window 2682 # to the root of the database. 2683 return self._ClosePopupAndRedirect("dir")
2684 2685
2686 - def HandleSubmitExpectationsForm(self, request):
2687 """Handle uploading expected results. 2688 2689 'request' -- The 'WebRequest' that caused the event.""" 2690 2691 # Clear out the current set of expected outcomes; the entire 2692 # set of new 2693 self.__expected_outcomes = {} 2694 2695 # Loop over all the tests. 2696 for id in self.GetDatabase().ExpandIds("")[0]: 2697 outcome = request[id] 2698 if outcome != "None": 2699 self.__expected_outcomes[id] = outcome 2700 2701 # Redirect to the main page. 2702 request = qm.web.WebRequest("dir", base=request) 2703 raise qm.web.HttpRedirect, request
2704 2705
2706 - def HandleSubmitItem(self, request):
2707 """Handle a test or resource submission. 2708 2709 This function handles submission of the test or resource editing form 2710 generated by 'handle_show'. The script name in 'request' should be 2711 'submit-test' or 'submit-resource'. It constructs the appropriate 2712 'Test' or 'Resource' object and writes it to the database, either as a 2713 new item or overwriting an existing item. 2714 2715 The request must have the following form fields: 2716 2717 'id' -- The test or resource ID of the item being edited or created. 2718 2719 'class' -- The name of the test or resource class of this item. 2720 2721 arguments -- Argument values are encoded in fields whose names start 2722 with 'qm.fields.Field.form_field_prefix'.""" 2723 2724 if request.GetScriptName() == "submit-test": 2725 type = "test" 2726 elif request.GetScriptName() == "submit-resource": 2727 type = "resource" 2728 2729 # Make sure there's an ID in the request, and extract it. 2730 try: 2731 item_id = request["id"] 2732 except KeyError: 2733 message = qm.error("no id for submit") 2734 return qm.web.generate_error_page(request, message) 2735 2736 database = self.__database 2737 # Learn whether or not this is a new item. 2738 is_new = int(request["is_new"]) 2739 # Extract the class and field specification. 2740 item_class_name = request["class"] 2741 item_class = qm.test.base.get_extension_class(item_class_name, 2742 type, 2743 database) 2744 fields = get_class_arguments(item_class) 2745 2746 # We'll perform various kinds of validation as we extract form 2747 # fields. Errors are placed into this map. 2748 field_errors = {} 2749 redisplay = 0 2750 2751 # Loop over fields of the class, looking for arguments in the 2752 # submitted request. 2753 arguments = {} 2754 temporary_store = self.GetTemporaryAttachmentStore() 2755 main_store = database.GetAttachmentStore() 2756 attachment_stores = { id(temporary_store): temporary_store, 2757 id(main_store): main_store } 2758 for field in fields: 2759 # Construct the name we expect for the corresponding argument. 2760 field_name = field.GetName() 2761 form_field_name = field.GetHtmlFormFieldName() 2762 # Parse the value for this field. 2763 try: 2764 value, r = field.ParseFormValue(request, form_field_name, 2765 attachment_stores) 2766 if r: 2767 redisplay = 1 2768 arguments[field_name] = value 2769 except: 2770 # Something went wrong parsing the value. Associate an 2771 # error message with this field. 2772 message = str(sys.exc_info()[1]) 2773 field_errors[field_name] = message 2774 redisplay = 1 2775 2776 if type == "test": 2777 # Create a new test. 2778 item = TestDescriptor( 2779 database, 2780 test_id=item_id, 2781 test_class_name=item_class_name, 2782 arguments=arguments) 2783 2784 elif type == "resource": 2785 # Create a new resource. 2786 item = ResourceDescriptor(database, item_id, 2787 item_class_name, arguments) 2788 2789 # If necessary, redisplay the form. 2790 if redisplay: 2791 request = qm.web.WebRequest("edit-" + type, base=request, 2792 id=item_id) 2793 return ShowItemPage(self, item, 1, is_new, type, 2794 field_errors)(request) 2795 2796 # Store it in the database. 2797 database.WriteExtension(item_id, item.GetItem()) 2798 2799 # Redirect to a page that displays the newly-edited item. 2800 request = qm.web.WebRequest("show-" + type, base=request, id=item_id) 2801 raise qm.web.HttpRedirect, request
2802 2803
2804 - def HandleSubmitResults(self, request):
2805 """Handle uploading results. 2806 2807 'request' -- The 'WebRequest' that caused the event.""" 2808 2809 # Get the results file data. 2810 data = request["file"] 2811 # Create a file object from the data. 2812 f = StringIO.StringIO(data) 2813 # Read the results. 2814 results = qm.test.base.load_results(f, self.GetDatabase()) 2815 # Enter them into a new results stream. 2816 self.__results_stream = StorageResultsStream() 2817 annotations = results.GetAnnotations() 2818 self.__results_stream.WriteAllAnnotations(annotations) 2819 for r in results: 2820 self.__results_stream.WriteResult(r) 2821 self.__results_stream.Summarize() 2822 # Close the upload popup window, and redirect the main window 2823 # to a view of the results. 2824 return self._ClosePopupAndRedirect("show-results")
2825 2826
2827 - def HandleSubmitSuite(self, request):
2828 """Handle test suite submission. 2829 2830 'request' -- A 'WebRequest' object. 2831 2832 The request object has these fields: 2833 2834 'id' -- The ID of the test suite being edited. If a suite with 2835 this ID exists, it is replaced (it must not be an implicit suite 2836 though). Otherwise a new suite is edited. 2837 2838 'test_ids' -- A comma-separated list of test IDs to include in the 2839 suite, relative to the suite's own ID. 2840 2841 'suite_ids' -- A comma-separated list of other test suite IDs to 2842 include in the suite, relative to the suite's own ID. 2843 """ 2844 2845 database = self.__database 2846 # Extract fields from the request. 2847 suite_id = request["id"] 2848 test_ids = request["test_ids"] 2849 if string.strip(test_ids) == "": 2850 test_ids = [] 2851 else: 2852 test_ids = string.split(test_ids, ",") 2853 suite_ids = request["suite_ids"] 2854 if string.strip(suite_ids) == "": 2855 suite_ids = [] 2856 else: 2857 suite_ids = string.split(suite_ids, ",") 2858 # Construct a new suite. 2859 suite_class = qm.test.base.get_extension_class( 2860 "explicit_suite.ExplicitSuite", 2861 "suite", 2862 self.GetDatabase()) 2863 extras = { suite_class.EXTRA_DATABASE : self.GetDatabase(), 2864 suite_class.EXTRA_ID : suite_id } 2865 suite = suite_class({ "test_ids" : test_ids, 2866 "suite_ids" : suite_ids }, 2867 **extras) 2868 # Store it. 2869 database.WriteExtension(suite_id, suite) 2870 # Redirect to a page that displays the newly-edited item. 2871 raise qm.web.HttpRedirect, \ 2872 qm.web.WebRequest("show-suite", base=request, id=suite_id)
2873 2874
2875 - def MakeNewTest(self, test_class_name, test_id):
2876 """Create a new test with default arguments. 2877 2878 'test_class_name' -- The name of the test class of which to create a 2879 new test. 2880 2881 'test_id' -- The test ID of the new test. 2882 2883 returns -- A new 'TestDescriptor' object.""" 2884 2885 test_class = qm.test.base.get_test_class(test_class_name, 2886 self.GetDatabase()) 2887 # Make sure there isn't already such a test. 2888 if self.GetDatabase().HasTest(test_id): 2889 raise RuntimeError, qm.error("test already exists", 2890 test_id=test_id) 2891 # Construct an argument map containing default values. 2892 arguments = {} 2893 for field in get_class_arguments(test_class): 2894 name = field.GetName() 2895 value = field.GetDefaultValue() 2896 arguments[name] = value 2897 # Construct a default test instance. 2898 return TestDescriptor(self.GetDatabase(), test_id, 2899 test_class_name, arguments)
2900 2901
2902 - def MakeNewResource(self, resource_class_name, resource_id):
2903 """Create a new resource with default arguments. 2904 2905 'resource_class_name' -- The name of the resource class of which to 2906 create a new resource. 2907 2908 'resource_id' -- The resource ID of the new resource. 2909 2910 returns -- A new 'ResourceDescriptor' object.""" 2911 2912 resource_class \ 2913 = qm.test.base.get_resource_class(resource_class_name, 2914 self.GetDatabase()) 2915 # Make sure there isn't already such a resource. 2916 if self.GetDatabase().HasResource(resource_id): 2917 raise RuntimeError, qm.error("resource already exists", 2918 resource_id=resource_id) 2919 # Construct an argument map containing default values. 2920 arguments = {} 2921 for field in get_class_arguments(resource_class): 2922 name = field.GetName() 2923 value = field.GetDefaultValue() 2924 arguments[name] = value 2925 # Construct a default resource instance. 2926 return ResourceDescriptor(self.GetDatabase(), resource_id, 2927 resource_class_name, arguments)
2928 2929
2930 - def _HandleRoot(self, request):
2931 """Handle the '/' URL.""" 2932 2933 raise qm.web.HttpRedirect, qm.web.WebRequest("/test/dir")
2934 2935
2936 - def _ClosePopupAndRedirect(self, url):
2937 """Close the current window. Redirect the main window to 'url'. 2938 2939 'url' -- A string giving the URL to which the main window should 2940 be redirected. 2941 2942 returns -- A string giving HTML that will close the current 2943 window and redirect the main window to 'url'.""" 2944 2945 return """<html><body><script language="JavaScript"> 2946 window.opener.location = '%s'; 2947 window.close();</script></body></html>""" % url
2948 2949 ######################################################################## 2950 # initialization 2951 ######################################################################## 2952 2953 # Use our 'DefaultDtmlPage' subclass even when generating generic 2954 # (non-QMTest) pages. 2955 #qm.web.DtmlPage.default_class = DefaultDtmlPage 2956 2957 ######################################################################## 2958 # Local Variables: 2959 # mode: python 2960 # indent-tabs-mode: nil 2961 # fill-column: 72 2962 # End: 2963