OLD | NEW |
1 #!/usr/bin/python2.4 | 1 #!/usr/bin/python2.4 |
2 # Copyright 2009, Google Inc. | 2 # Copyright 2009, Google Inc. |
3 # All rights reserved. | 3 # All rights reserved. |
4 # | 4 # |
5 # Redistribution and use in source and binary forms, with or without | 5 # Redistribution and use in source and binary forms, with or without |
6 # modification, are permitted provided that the following conditions are | 6 # modification, are permitted provided that the following conditions are |
7 # met: | 7 # met: |
8 # | 8 # |
9 # * Redistributions of source code must retain the above copyright | 9 # * Redistributions of source code must retain the above copyright |
10 # notice, this list of conditions and the following disclaimer. | 10 # notice, this list of conditions and the following disclaimer. |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
54 import re | 54 import re |
55 import SimpleHTTPServer | 55 import SimpleHTTPServer |
56 import socket | 56 import socket |
57 import SocketServer | 57 import SocketServer |
58 import subprocess | 58 import subprocess |
59 import threading | 59 import threading |
60 import time | 60 import time |
61 import unittest | 61 import unittest |
62 import gflags | 62 import gflags |
63 import javascript_unit_tests | 63 import javascript_unit_tests |
64 # Import custom testrunner for pulse | 64 import test_runner |
65 import pulse_testrunner | |
66 import selenium | 65 import selenium |
67 import samples_tests | 66 import samples_tests |
68 import selenium_constants | 67 import selenium_constants |
69 import selenium_utilities | 68 import selenium_utilities |
| 69 import pdiff_test |
| 70 import Queue |
70 | 71 |
71 if sys.platform == 'win32' or sys.platform == 'cygwin': | 72 if sys.platform == 'win32' or sys.platform == 'cygwin': |
72 default_java_exe = "java.exe" | 73 default_java_exe = "java.exe" |
73 else: | 74 else: |
74 default_java_exe = "java" | 75 default_java_exe = "java" |
75 | 76 |
76 # Command line flags | 77 # Command line flags |
77 FLAGS = gflags.FLAGS | 78 FLAGS = gflags.FLAGS |
78 gflags.DEFINE_boolean("verbose", False, "verbosity") | 79 gflags.DEFINE_boolean("verbose", False, "verbosity") |
79 gflags.DEFINE_boolean("screenshots", False, "takes screenshots") | 80 gflags.DEFINE_boolean("screenshots", False, "takes screenshots") |
(...skipping 27 matching lines...) Expand all Loading... |
107 "specifies the prefix of tests to run") | 108 "specifies the prefix of tests to run") |
108 gflags.DEFINE_string( | 109 gflags.DEFINE_string( |
109 "testsuffixes", | 110 "testsuffixes", |
110 "small,medium,large", | 111 "small,medium,large", |
111 "specifies the suffixes, separated by commas of tests to run") | 112 "specifies the suffixes, separated by commas of tests to run") |
112 gflags.DEFINE_string( | 113 gflags.DEFINE_string( |
113 "servertimeout", | 114 "servertimeout", |
114 "30", | 115 "30", |
115 "Specifies the timeout value, in seconds, for the selenium server.") | 116 "Specifies the timeout value, in seconds, for the selenium server.") |
116 | 117 |
| 118 |
117 # Browsers to choose from (for browser flag). | 119 # Browsers to choose from (for browser flag). |
118 # use --browser $BROWSER_NAME to run | 120 # use --browser $BROWSER_NAME to run |
119 # tests for that browser | 121 # tests for that browser |
120 gflags.DEFINE_list( | 122 gflags.DEFINE_list( |
121 "browser", | 123 "browser", |
122 "*firefox", | 124 "*firefox", |
123 "\n".join(["comma-separated list of browsers to test", | 125 "\n".join(["comma-separated list of browsers to test", |
124 "Options:"] + | 126 "Options:"] + |
125 selenium_constants.SELENIUM_BROWSER_SET)) | 127 selenium_constants.SELENIUM_BROWSER_SET)) |
126 gflags.DEFINE_string( | 128 gflags.DEFINE_string( |
127 "browserpath", | 129 "browserpath", |
128 "", | 130 "", |
129 "specifies the path to the browser executable " | 131 "specifies the path to the browser executable " |
130 "(for platforms that don't support MOZ_PLUGIN_PATH)") | 132 "(for platforms that don't support MOZ_PLUGIN_PATH)") |
131 gflags.DEFINE_string( | 133 gflags.DEFINE_string( |
132 "samplespath", | 134 "samplespath", |
133 "", | 135 "", |
134 "specifies the path from the web root to the samples.") | 136 "specifies the path from the web root to the samples.") |
135 | 137 |
136 | |
137 class MyRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): | 138 class MyRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): |
138 """Hook to handle HTTP server requests. | 139 """Hook to handle HTTP server requests. |
139 | 140 |
140 Functions as a handler for logging and other utility functions. | 141 Functions as a handler for logging and other utility functions. |
141 """ | 142 """ |
142 | 143 |
143 def log_message(self, format, *args): | 144 def log_message(self, format, *args): |
144 """Logging hook for HTTP server.""" | 145 """Logging hook for HTTP server.""" |
145 | 146 |
146 # For now, just suppress logging. | 147 # For now, just suppress logging. |
147 pass | 148 pass |
148 # TODO: might be nice to have a verbose option for debugging. | 149 # TODO: might be nice to have a verbose option for debugging. |
149 | 150 |
| 151 |
| 152 |
150 | 153 |
151 class LocalFileHTTPServer(threading.Thread): | 154 class LocalFileHTTPServer(threading.Thread): |
152 """Minimal HTTP server that serves local files. | 155 """Minimal HTTP server that serves local files. |
153 | 156 |
154 Members: | 157 Members: |
155 http_alive: event to signal that http server is up and running | 158 http_alive: event to signal that http server is up and running |
156 http_port: the TCP port the server is using | 159 http_port: the TCP port the server is using |
157 """ | 160 """ |
158 | 161 |
159 START_PORT = 8100 | 162 START_PORT = 8100 |
(...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
341 if not selenium_server.selenium_port: | 344 if not selenium_server.selenium_port: |
342 print 'Timed out.' | 345 print 'Timed out.' |
343 return None | 346 return None |
344 | 347 |
345 print("Selenium RC server started on port %d" | 348 print("Selenium RC server started on port %d" |
346 % selenium_server.selenium_port) | 349 % selenium_server.selenium_port) |
347 | 350 |
348 return selenium_server | 351 return selenium_server |
349 | 352 |
350 | 353 |
351 class SeleniumSession(object): | 354 class SeleniumSessionBuilder: |
352 """A selenium browser session, with support servers. | 355 def __init__(self, sel_port, sel_timeout, http_port, browserpath): |
353 | 356 |
354 The support servers include a Selenium Remote Control server, and | 357 self.sel_port = sel_port |
355 a local HTTP server to serve static test files. | 358 self.sel_timeout = sel_timeout |
| 359 self.http_port = http_port |
| 360 self.browserpath = browserpath |
356 | 361 |
357 Members: | 362 def NewSeleniumSession(self, browser): |
358 session: a selenium() instance | |
359 selenium_server: a SeleniumRemoteControl() instance | |
360 http_server: a LocalFileHTTPServer() instance | |
361 runner: a TestRunner() instance | |
362 """ | |
363 | |
364 def __init__(self, verbose, java_path, selenium_server, server_timeout, | |
365 http_root=None): | |
366 """Initializes a Selenium Session. | |
367 | |
368 Args: | |
369 verbose: boolean verbose flag | |
370 java_path: path to java used to run selenium. | |
371 selenium_server: path to jar containing selenium server. | |
372 server_timeout: server timeout value, in seconds. | |
373 http_root: Serve http pages using this path as the document root. When | |
374 None, use the default. | |
375 """ | |
376 # Start up a static file server, to serve the test pages. | |
377 | |
378 if not http_root: | |
379 http_root = FLAGS.product_dir | |
380 | |
381 self.http_server = LocalFileHTTPServer.StartServer(http_root) | |
382 | |
383 if self.http_server: | |
384 # Start up the Selenium Remote Control Server | |
385 self.selenium_server = SeleniumRemoteControl.StartServer(verbose, | |
386 java_path, | |
387 selenium_server, | |
388 server_timeout) | |
389 if not self.http_server or not self.selenium_server: | |
390 return | |
391 | |
392 # Set up a testing runner | |
393 self.runner = pulse_testrunner.PulseTestRunner() | |
394 | |
395 # Set up a phantom selenium session so we can call shutdown if needed. | |
396 self.session = selenium.selenium( | |
397 "localhost", self.selenium_server.selenium_port, "*firefox", | |
398 "http://" + socket.gethostname() + ":" + | |
399 str(self.http_server.http_port)) | |
400 | |
401 def StartSession(self, browser): | |
402 """Starts the Selenium Session and connects to the HTTP server. | |
403 | |
404 Args: | |
405 browser: selenium browser name | |
406 """ | |
407 | |
408 if browser == "*googlechrome": | 363 if browser == "*googlechrome": |
409 # TODO: Replace socket.gethostname() with "localhost" | 364 # TODO: Replace socket.gethostname() with "localhost" |
410 # once Chrome local proxy fix is in. | 365 # once Chrome local proxy fix is in. |
411 server_url = "http://" + socket.gethostname() + ":" | 366 server_url = "http://" + socket.gethostname() + ":" |
412 else: | 367 else: |
413 server_url = "http://localhost:" | 368 server_url = "http://localhost:" |
414 server_url += str(self.http_server.http_port) | 369 server_url += str(self.http_port) |
415 | 370 |
416 browser_path_with_space = "" | 371 browser_path_with_space = "" |
417 if FLAGS.browserpath: | 372 if self.browserpath: |
418 browser_path_with_space = " " + FLAGS.browserpath | 373 browser_path_with_space = " " + self.browserpath |
419 | |
420 self.session = selenium.selenium("localhost", | |
421 self.selenium_server.selenium_port, | |
422 browser + browser_path_with_space, | |
423 server_url) | |
424 self.session.start() | |
425 | |
426 def CloseSession(self): | |
427 """Closes the selenium sesssion.""" | |
428 self.session.stop() | |
429 | |
430 def TearDown(self): | |
431 """Stops the selenium server.""" | |
432 self.session.shut_down_selenium_server() | |
433 | |
434 def TestBrowser(self, browser, test_list, test_prefix, test_suffixes, | |
435 server_timeout): | |
436 """Runs Selenium tests for a specific browser. | |
437 | |
438 Args: | |
439 browser: selenium browser name (eg. *iexplore, *firefox). | |
440 test_list: list to add tests to. | |
441 test_prefix: prefix of tests to run. | |
442 test_suffixes: comma separated suffixes of tests to run. | |
443 server_timeout: server timeout value, in milliseconds | |
444 | |
445 Returns: | |
446 result: result of test runner. | |
447 """ | |
448 print "Testing %s..." % browser | |
449 self.StartSession(browser) | |
450 self.session.set_timeout(server_timeout) | |
451 self.runner.setBrowser(browser) | |
452 | |
453 try: | |
454 result = self.runner.run( | |
455 SeleniumSuite(self.session, browser, test_list, | |
456 test_prefix, test_suffixes)) | |
457 finally: | |
458 self.CloseSession() | |
459 | |
460 return result | |
461 | 374 |
462 | 375 |
463 class LocalTestSuite(unittest.TestSuite): | 376 new_session = selenium.selenium("localhost", |
464 """Wrapper for unittest.TestSuite so we can collect the tests.""" | 377 self.sel_port, |
| 378 browser + browser_path_with_space, |
| 379 server_url) |
| 380 |
| 381 new_session.start() |
| 382 new_session.set_timeout(self.sel_timeout) |
| 383 |
| 384 return new_session |
465 | 385 |
466 def __init__(self): | |
467 unittest.TestSuite.__init__(self) | |
468 self.test_list = [] | |
469 | 386 |
470 def addTest(self, name, test): | 387 def TestBrowser(session_builder, browser, test_list): |
471 """Adds a test to the TestSuite and records its name and test_path. | 388 """Runs Selenium tests for a specific browser. |
472 | 389 |
473 Args: | 390 Args: |
474 name: name of test. | 391 session_builder: session_builder for creating new selenium sessions. |
475 test: test to pass to unittest.TestSuite. | 392 browser: selenium browser name (eg. *iexplore, *firefox). |
476 """ | 393 test_list: list of tests. |
477 unittest.TestSuite.addTest(self, test) | 394 |
478 try: | 395 Returns: |
479 self.test_list.append((name, test.options)) | 396 summary_result: result of test runners. |
480 except AttributeError: | 397 """ |
481 self.test_list.append((name, [])) | 398 print "Testing %s..." % browser |
| 399 |
| 400 summary_result = test_runner.TestResult(test_runner.StringBuffer(), browser) |
| 401 |
| 402 # Fill up the selenium test queue. |
| 403 test_queue = Queue.Queue() |
| 404 for test in test_list: |
| 405 test_queue.put(test) |
| 406 |
| 407 |
| 408 pdiff_queue = None |
| 409 if FLAGS.screenshots: |
| 410 # Need to do screen comparisons. |
| 411 # |pdiff_queue| is the queue of perceptual diff tests that need to be done. |
| 412 # This queue is added to by individual slenium test runners. |
| 413 # |pdiff_result_queue| is the result of the perceptual diff tests. |
| 414 pdiff_queue = Queue.Queue() |
| 415 pdiff_result_queue = Queue.Queue() |
| 416 pdiff_worker = test_runner.PDiffTestRunner(pdiff_queue, |
| 417 pdiff_result_queue, |
| 418 browser) |
| 419 pdiff_worker.start() |
| 420 |
| 421 # Start initial selenium test runner. |
| 422 worker = test_runner.SeleniumTestRunner(session_builder, browser, |
| 423 test_queue, pdiff_queue) |
| 424 worker.start() |
| 425 |
| 426 # Run through all selenium tests. |
| 427 while not worker.IsCompletelyDone(): |
| 428 if worker.IsTesting() and worker.IsPastDeadline(): |
| 429 # Test has taken more than allotted. Abort and go to next test. |
| 430 worker.AbortTest() |
| 431 |
| 432 elif worker.DidFinishTest(): |
| 433 # Do this so that a worker does not grab test off queue till we tell it.
|
| 434 result = worker.Continue() |
| 435 result.printAll(sys.stdout) |
| 436 summary_result.merge(result) |
| 437 |
| 438 if FLAGS.screenshots: |
| 439 # Finish screenshot comparisons. |
| 440 pdiff_worker.EndTesting() |
| 441 while not pdiff_worker.IsCompletelyDone(): |
| 442 time.sleep(1) |
| 443 |
| 444 # Be careful here, make sure no one else is editing |pdiff_reult_queue|. |
| 445 while not pdiff_result_queue.empty(): |
| 446 result = pdiff_result_queue.get() |
| 447 result.printAll(sys.stdout) |
| 448 summary_result.merge(result) |
| 449 |
| 450 return summary_result |
| 451 |
482 | 452 |
483 | 453 |
484 def MatchesSuffix(name, suffixes): | 454 def MatchesSuffix(name, suffixes): |
485 """Checks if a name ends in one of the suffixes. | 455 """Checks if a name ends in one of the suffixes. |
486 | 456 |
487 Args: | 457 Args: |
488 name: Name to test. | 458 name: Name to test. |
489 suffixes: list of suffixes to test for. | 459 suffixes: list of suffixes to test for. |
490 Returns: | 460 Returns: |
491 True if name ends in one of the suffixes or if suffixes is empty. | 461 True if name ends in one of the suffixes or if suffixes is empty. |
492 """ | 462 """ |
493 if suffixes: | 463 if suffixes: |
494 name_lower = name.lower() | 464 name_lower = name.lower() |
495 for suffix in suffixes: | 465 for suffix in suffixes: |
496 if name_lower.endswith(suffix): | 466 if name_lower.endswith(suffix): |
497 return True | 467 return True |
498 return False | 468 return False |
499 else: | 469 else: |
500 return True | 470 return True |
501 | 471 |
502 | 472 |
503 def AddTests(test_suite, session, browser, module, filename, prefix, | 473 def _GetTestsFromFile(filename, prefix, test_prefix_filter, test_suffixes, |
504 test_prefix_filter, test_suffixes, path_to_html): | 474 browser, module, path_to_html): |
505 """Add tests defined in filename. | 475 """Add tests defined in filename, and associated perceptual diff test, if |
| 476 needed. |
506 | 477 |
507 Assumes module has a method "GenericTest" that uses self.args to run. | 478 Assumes module has a method "GenericTest" that uses self.args to run. |
508 | 479 |
509 Args: | 480 Args: |
510 test_suite: A Selenium test_suite to add tests to. | |
511 session: a Selenium instance. | |
512 browser: browser name. | |
513 module: module which will have method GenericTest() called to run each test. | |
514 filename: filename of file with list of tests. | 481 filename: filename of file with list of tests. |
515 prefix: prefix to add to the beginning of each test. | 482 prefix: prefix to add to the beginning of each test. |
516 test_prefix_filter: Only adds a test if it starts with this. | 483 test_prefix_filter: Only adds a test if it starts with this. |
517 test_suffixes: list of suffixes to filter by. An empty list = pass all. | 484 test_suffixes: list of suffixes to filter by. An empty list = pass all. |
| 485 browser: browser name. |
| 486 module: module which will have method GenericTest() called to run each test.
|
518 path_to_html: Path from server root to html | 487 path_to_html: Path from server root to html |
519 """ | 488 """ |
520 # See comments in that file for the expected format. | 489 # See comments in that file for the expected format. |
521 # skip lines that are blank or have "#" or ";" as their first non whitespace | 490 # skip lines that are blank or have "#" or ";" as their first non whitespace |
522 # character. | 491 # character. |
523 test_list_file = open(filename, "r") | 492 test_list_file = open(filename, "r") |
524 samples = test_list_file.readlines() | 493 samples = test_list_file.readlines() |
525 test_list_file.close() | 494 test_list_file.close() |
526 | 495 |
| 496 tests = [] |
| 497 |
527 for sample in samples: | 498 for sample in samples: |
528 sample = sample.strip() | 499 sample = sample.strip() |
529 if not sample or sample[0] == ";" or sample[0] == "#": | 500 if not sample or sample[0] == ";" or sample[0] == "#": |
530 continue | 501 continue |
531 | 502 |
532 arguments = sample.split() | 503 arguments = sample.split() |
533 test_type = arguments[0].lower() | 504 test_type = arguments[0].lower() |
534 test_path = arguments[1] | 505 test_path = arguments[1] |
535 options = arguments[2:] | 506 options = arguments[2:] |
536 | 507 |
537 # TODO: Add filter based on test_type | 508 # TODO: Add filter based on test_type |
538 | 509 if test_path.startswith("Test"): |
539 name = ("Test" + prefix + re.sub("\W", "_", test_path) + | 510 name = test_path |
540 test_type.capitalize()) | 511 else: |
| 512 # Need to make a name. |
| 513 name = ("Test" + prefix + re.sub("\W", "_", test_path) + |
| 514 test_type.capitalize()) |
541 | 515 |
542 # Only execute this test if the current browser is not in the list | 516 # Only execute this test if the current browser is not in the list |
543 # of skipped browsers. | 517 # of skipped browsers. |
544 test_skipped = False | 518 test_skipped = False |
| 519 screenshot_count = 0 |
545 for option in options: | 520 for option in options: |
546 if option.startswith("except"): | 521 if option.startswith("except"): |
547 skipped_platforms = selenium_utilities.GetArgument(option) | 522 skipped_platforms = selenium_utilities.GetArgument(option) |
548 if not skipped_platforms is None: | 523 if not skipped_platforms is None: |
549 skipped_platforms = skipped_platforms.split(",") | 524 skipped_platforms = skipped_platforms.split(",") |
550 if browser in skipped_platforms: | 525 if browser in skipped_platforms: |
551 test_skipped = True | 526 test_skipped = True |
| 527 elif option.startswith("screenshots"): |
| 528 screenshot_count += int(selenium_utilities.GetArgument(option)) |
| 529 elif option.startswith("screenshot"): |
| 530 screenshot_count += 1 |
| 531 |
| 532 if (test_prefix_filter and not name.startswith(test_prefix_filter) or |
| 533 test_suffixes and not MatchesSuffix(name, test_suffixes)): |
| 534 test_skipped = True |
552 | 535 |
553 if not test_skipped: | 536 if not test_skipped: |
554 # Check if there is already a test function by this name in the module. | 537 # Add a test method with this name if it doesn't exist. |
555 if (test_path.startswith(test_prefix_filter) and | 538 if not (hasattr(module, name) and callable(getattr(module, name))): |
556 hasattr(module, test_path) and callable(getattr(module, test_path))): | |
557 test_suite.addTest(test_path, module(test_path, session, browser, | |
558 path_to_html, options=options)) | |
559 elif (name.startswith(test_prefix_filter) and | |
560 MatchesSuffix(name, test_suffixes)): | |
561 # no, so add a method that will run a test generically. | |
562 setattr(module, name, module.GenericTest) | 539 setattr(module, name, module.GenericTest) |
563 test_suite.addTest(name, module(name, session, browser, path_to_html, | 540 |
564 test_type, test_path, options)) | 541 new_test = module(name, browser, path_to_html, test_type, test_path, |
| 542 options) |
| 543 |
| 544 if screenshot_count and FLAGS.screenshots: |
| 545 pdiff_name = name + 'Screenshots' |
| 546 screenshot = selenium_utilities.ScreenshotNameFromTestName(test_path) |
| 547 setattr(pdiff_test.PDiffTest, pdiff_name, |
| 548 pdiff_test.PDiffTest.PDiffTest) |
| 549 new_pdiff = pdiff_test.PDiffTest(pdiff_name, |
| 550 screenshot_count, |
| 551 screenshot, |
| 552 FLAGS.screencompare, |
| 553 FLAGS.screenshotsdir, |
| 554 FLAGS.referencedir, |
| 555 options) |
| 556 tests += [(new_test, new_pdiff)] |
| 557 else: |
| 558 tests += [new_test] |
| 559 |
| 560 |
| 561 return tests |
565 | 562 |
566 | 563 |
567 def SeleniumSuite(session, browser, test_list, test_prefix, test_suffixes): | 564 def GetTestsForBrowser(browser, test_prefix, test_suffixes): |
568 """Creates a test suite to run the unit tests. | 565 """Returns list of tests from test files. |
569 | 566 |
570 Args: | 567 Args: |
571 session: a selenium() instance | |
572 browser: browser name | 568 browser: browser name |
573 test_list: list to add tests to. | |
574 test_prefix: prefix of tests to run. | 569 test_prefix: prefix of tests to run. |
575 test_suffixes: A comma separated string of suffixes to filter by. | 570 test_suffixes: A comma separated string of suffixes to filter by. |
576 Returns: | 571 Returns: |
577 A selenium test suite. | 572 A list of unittest.TestCase. |
578 """ | 573 """ |
579 | 574 tests = [] |
580 test_suite = LocalTestSuite() | |
581 | |
582 suffixes = test_suffixes.split(",") | 575 suffixes = test_suffixes.split(",") |
583 | 576 |
584 # add sample tests. | 577 # add sample tests. |
585 filename = os.path.abspath(os.path.join(script_dir, "sample_list.txt")) | 578 filename = os.path.abspath(os.path.join(script_dir, "sample_list.txt")) |
586 AddTests(test_suite, | 579 tests += _GetTestsFromFile(filename, "Sample", test_prefix, suffixes, browser, |
587 session, | 580 samples_tests.SampleTests, |
588 browser, | 581 FLAGS.samplespath.replace("\\","/")) |
589 samples_tests.SampleTests, | 582 |
590 filename, | |
591 "Sample", | |
592 test_prefix, | |
593 suffixes, | |
594 FLAGS.samplespath.replace("\\", "/")) | |
595 | |
596 # add javascript tests. | 583 # add javascript tests. |
597 filename = os.path.abspath(os.path.join(script_dir, | 584 filename = os.path.abspath(os.path.join(script_dir, |
598 "javascript_unit_test_list.txt")) | 585 "javascript_unit_test_list.txt")) |
599 AddTests(test_suite, | 586 tests += _GetTestsFromFile(filename, "UnitTest", test_prefix, suffixes, |
600 session, | 587 browser, javascript_unit_tests.JavaScriptUnitTests, |
601 browser, | 588 "") |
602 javascript_unit_tests.JavaScriptUnitTests, | |
603 filename, | |
604 "UnitTest", | |
605 test_prefix, | |
606 suffixes, | |
607 '') | |
608 | 589 |
609 test_list += test_suite.test_list | 590 return tests |
610 | |
611 return test_suite | |
612 | 591 |
613 | 592 |
614 def CompareScreenshots(browser, test_list, screencompare, screenshotsdir, | 593 def GetChromePath(): |
615 screencompare_tool, verbose): | 594 value = None |
616 """Performs the image validation for test-case frame captures. | 595 if sys.platform == "win32" or sys.platform == "cygwin": |
617 | 596 import _winreg |
618 Args: | 597 try: |
619 browser: selenium browser name | 598 key = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT, |
620 test_list: list of tests that ran. | 599 "Applications\\chrome.exe\\shell\\open\\command") |
621 screencompare: True to actually run tests. | 600 (value, type) = _winreg.QueryValueEx(key, None) |
622 screenshotsdir: path of directory containing images to compare. | 601 _winreg.CloseKey(key) |
623 screencompare_tool: path to image diff tool. | 602 value = os.path.dirname(value) |
624 verbose: If True then outputs verbose info. | 603 |
625 | 604 except WindowsError: |
626 Returns: | 605 value = None |
627 A Results object. | 606 if '*googlechrome' in FLAGS.browser: |
628 """ | 607 raise Exception("Unable to determine location for Chrome -- " + |
629 print "Validating captured frames against reference data..." | 608 "is it installed?") |
630 | 609 |
631 class Results(object): | 610 return value |
632 """An object to return results of screenshot compares. | |
633 | |
634 Similar to unittest.TestResults. | |
635 """ | |
636 | |
637 def __init__(self): | |
638 object.__init__(self) | |
639 self.tests_run = 0 | |
640 self.current_test = None | |
641 self.errors = [] | |
642 self.failures = [] | |
643 self.start_time = 0 | |
644 | |
645 def StartTest(self, test): | |
646 """Adds a test. | |
647 | |
648 Args: | |
649 test: name of test. | |
650 """ | |
651 self.start_time = time.time() | |
652 self.tests_run += 1 | |
653 self.current_test = test | |
654 | |
655 def TimeTaken(self): | |
656 """Returns the time since the last call to StartTest.""" | |
657 return time.time() - self.start_time | |
658 | |
659 def AddFailure(self, test, browser, message): | |
660 """Adds a failure. | |
661 | |
662 Args: | |
663 test: name of the test. | |
664 browser: name of the browser. | |
665 message: error message to print | |
666 """ | |
667 self.failures.append(test) | |
668 print "ERROR: ", message | |
669 print("SELENIUMRESULT %s <%s> [%.3fs]: FAIL" | |
670 % (test, browser, self.TimeTaken())) | |
671 | |
672 def AddSuccess(self, test): | |
673 """Adds a success. | |
674 | |
675 Args: | |
676 test: name of the test. | |
677 """ | |
678 print("SELENIUMRESULT %s <%s> [%.3fs]: PASS" | |
679 % (test, browser, self.TimeTaken())) | |
680 | |
681 def WasSuccessful(self): | |
682 """Returns true if all tests were successful.""" | |
683 return not self.errors and not self.failures | |
684 | |
685 results = Results() | |
686 | |
687 if not screencompare: | |
688 return results | |
689 | |
690 base_path = os.getcwd() | |
691 | |
692 reference_files = os.listdir(os.path.join( | |
693 base_path, | |
694 selenium_constants.REFERENCE_SCREENSHOT_PATH)) | |
695 | |
696 generated_files = os.listdir(os.path.join(base_path, screenshotsdir)) | |
697 | |
698 # Prep the test list for matching | |
699 temp = [] | |
700 for (test, options) in test_list: | |
701 test = selenium_utilities.StripTestTypeSuffix(test) | |
702 temp.append((test.lower(), options)) | |
703 test_list = temp | |
704 | |
705 # Create regex object for filename | |
706 # file is in format "FILENAME_reference.png" | |
707 reference_file_name_regex = re.compile(r"^(.*)_reference\.png") | |
708 generated_file_name_regex = re.compile(r"^(.*)\.png") | |
709 | |
710 # check that there is a reference file for each generated file. | |
711 for file_name in generated_files: | |
712 match = generated_file_name_regex.search(file_name) | |
713 | |
714 if match is None: | |
715 # no matches | |
716 continue | |
717 | |
718 # Generated file name without png extension | |
719 actual_name = match.group(1) | |
720 | |
721 # Get full paths to reference and generated files | |
722 reference_file = os.path.join( | |
723 base_path, | |
724 selenium_constants.REFERENCE_SCREENSHOT_PATH, | |
725 actual_name + "_reference.png") | |
726 generated_file = os.path.join( | |
727 base_path, | |
728 screenshotsdir, | |
729 actual_name + ".png") | |
730 | |
731 test_name = "TestReferenceScreenshotExists_" + actual_name | |
732 results.StartTest(test_name) | |
733 if not os.path.exists(reference_file): | |
734 # reference file does not exist | |
735 results.AddFailure( | |
736 test_name, browser, | |
737 "Missing reference file %s for generated file %s." % | |
738 (reference_file, generated_file)) | |
739 else: | |
740 results.AddSuccess(test_name) | |
741 | |
742 # Assuming both the result and reference image sets are the same size, | |
743 # verify that corresponding images are similar within tolerance. | |
744 for file_name in reference_files: | |
745 match = reference_file_name_regex.search(file_name) | |
746 | |
747 if match is None: | |
748 # no matches | |
749 continue | |
750 | |
751 # Generated file name without png extension | |
752 actual_name = match.group(1) | |
753 # Get full paths to reference and generated files | |
754 reference_file = os.path.join( | |
755 base_path, | |
756 selenium_constants.REFERENCE_SCREENSHOT_PATH, | |
757 file_name) | |
758 platform_specific_reference_file = os.path.join( | |
759 base_path, | |
760 selenium_constants.PLATFORM_SPECIFIC_REFERENCE_SCREENSHOT_PATH, | |
761 actual_name + "_reference.png") | |
762 generated_file = os.path.join( | |
763 base_path, | |
764 screenshotsdir, | |
765 actual_name + ".png") | |
766 | |
767 # Generate a test case name | |
768 test_name = "TestScreenCompare_" + actual_name | |
769 | |
770 # skip the reference file if the test is not in the test list. | |
771 basename = os.path.splitext(os.path.basename(file_name))[0] | |
772 basename = re.sub("\d+_reference", "", basename).lower() | |
773 basename = re.sub("\W", "_", basename) | |
774 test_was_run = False | |
775 test_options = [] | |
776 for (test, options) in test_list: | |
777 if test.endswith(basename): | |
778 test_was_run = True | |
779 test_options = options or [] | |
780 break | |
781 | |
782 if test_was_run: | |
783 results.StartTest(test_name) | |
784 else: | |
785 # test was not planned to run for this reference image. | |
786 if os.path.exists(generated_file): | |
787 # a generated file exists? The test name does not match the screenshot. | |
788 results.StartTest(test_name) | |
789 results.AddFailure(test_name, browser, | |
790 "Test name and screenshot name do not match.") | |
791 continue | |
792 | |
793 # Check if there is a platform specific version of the reference image | |
794 if os.path.exists(platform_specific_reference_file): | |
795 reference_file = platform_specific_reference_file | |
796 | |
797 # Check if perceptual diff exists | |
798 pdiff_path = os.path.join(base_path, screencompare_tool) | |
799 if not os.path.exists(pdiff_path): | |
800 # Perceptualdiff.exe does not exist, fail. | |
801 results.AddFailure( | |
802 test_name, browser, | |
803 "Perceptual diff %s does not exist." % pdiff_path) | |
804 continue | |
805 | |
806 pixel_threshold = "10" | |
807 alpha_threshold = "1.0" | |
808 use_colorfactor = False | |
809 use_downsample = False | |
810 use_edge = True | |
811 edge_threshold = "5" | |
812 | |
813 # Find out if the test specified any options relating to perceptual diff | |
814 # that will override the defaults. | |
815 for opt in test_options: | |
816 if opt.startswith("pdiff_threshold"): | |
817 pixel_threshold = selenium_utilities.GetArgument(opt) | |
818 elif (opt.startswith("pdiff_threshold_mac") and | |
819 sys.platform == "darwin"): | |
820 pixel_threshold = selenium_utilities.GetArgument(opt) | |
821 elif (opt.startswith("pdiff_threshold_win") and | |
822 sys.platform == 'win32' or sys.platform == "cygwin"): | |
823 pixel_threshold = selenium_utilities.GetArgument(opt) | |
824 elif (opt.startswith("pdiff_threshold_linux") and | |
825 sys.platform[:5] == "linux"): | |
826 pixel_threshold = selenium_utilities.GetArgument(opt) | |
827 elif (opt.startswith("colorfactor")): | |
828 colorfactor = selenium_utilities.GetArgument(opt) | |
829 use_colorfactor = True | |
830 elif (opt.startswith("downsample")): | |
831 downsample_factor = selenium_utilities.GetArgument(opt) | |
832 use_downsample = True | |
833 elif (opt.startswith("pdiff_edge_ignore_off")): | |
834 use_edge = False | |
835 elif (opt.startswith("pdiff_edge_threshold")): | |
836 edge_threshold = selenium_utilities.GetArgument(opt) | |
837 | |
838 # Check if file exists | |
839 if os.path.exists(generated_file): | |
840 diff_file = os.path.join(base_path, screenshotsdir, | |
841 "compare_%s.png" % actual_name) | |
842 | |
843 # Run perceptual diff | |
844 arguments = [pdiff_path, | |
845 reference_file, | |
846 generated_file, | |
847 "-output", diff_file, | |
848 "-fov", "45", | |
849 "-alphaThreshold", alpha_threshold, | |
850 # Turn on verbose output for the percetual diff so we | |
851 # can see how far off we are on the threshold. | |
852 "-verbose", | |
853 # Set the threshold to zero so we can get a count | |
854 # of the different pixels. This causes the program | |
855 # to return failure for most images, but we can compare | |
856 # the values ourselves below. | |
857 "-threshold", "0"] | |
858 if use_colorfactor: | |
859 arguments += ["-colorfactor", colorfactor] | |
860 if use_downsample: | |
861 arguments += ["-downsample", downsample_factor] | |
862 if use_edge: | |
863 arguments += ["-ignoreEdges", edge_threshold] | |
864 | |
865 # Print the perceptual diff command line so we can debug easier. | |
866 if verbose: | |
867 print " ".join(arguments) | |
868 | |
869 # diff tool should return 0 on success | |
870 expected_result = 0 | |
871 | |
872 pdiff_pipe = subprocess.Popen(arguments, | |
873 stdout=subprocess.PIPE, | |
874 stderr=subprocess.PIPE) | |
875 (pdiff_stdout, pdiff_stderr) = pdiff_pipe.communicate() | |
876 result = pdiff_pipe.returncode | |
877 | |
878 # Find out how many pixels were different by looking at the output. | |
879 pixel_re = re.compile("(\d+) pixels are different", re.DOTALL) | |
880 pixel_match = pixel_re.search(pdiff_stdout) | |
881 different_pixels = "0" | |
882 if pixel_match: | |
883 different_pixels = pixel_match.group(1) | |
884 | |
885 alpha_re = re.compile("max alpha delta of ([0-9\.]+)", re.DOTALL) | |
886 alpha_delta = "0.0" | |
887 alpha_match = alpha_re.search(pdiff_stdout) | |
888 if alpha_match: | |
889 alpha_delta = alpha_match.group(1) | |
890 | |
891 if (result == expected_result or (pixel_match and | |
892 int(different_pixels) <= int(pixel_threshold))): | |
893 # The perceptual diff passed. | |
894 pass_re = re.compile("PASS: (.*?)\n", re.DOTALL) | |
895 pass_match = pass_re.search(pdiff_stdout) | |
896 reason = "Images are not perceptually different." | |
897 if pass_match: | |
898 reason = pass_match.group(1) | |
899 print ("%s PASSED with %s different pixels " | |
900 "(threshold %s) because: %s" % (test_name, | |
901 different_pixels, | |
902 pixel_threshold, | |
903 reason)) | |
904 results.AddSuccess(test_name) | |
905 else: | |
906 # The perceptual diff failed. | |
907 if pixel_match and int(different_pixels) > int(pixel_threshold): | |
908 results.AddFailure( | |
909 test_name, browser, | |
910 ("Reference framebuffer (%s) does not match generated " | |
911 "file (%s): %s non-matching pixels, max alpha delta: %s, " | |
912 "threshold: %s, alphaThreshold: %s." % | |
913 (reference_file, generated_file, different_pixels, alpha_delta, | |
914 pixel_threshold, alpha_threshold))) | |
915 else: | |
916 # The perceptual diff failed for some reason other than | |
917 # pixel differencing. | |
918 fail_re = re.compile("FAIL: (.*?)\n", re.DOTALL) | |
919 fail_match = fail_re.search(pdiff_stdout) | |
920 reason = "Unknown failure" | |
921 if fail_match: | |
922 reason = fail_match.group(1) | |
923 results.AddFailure( | |
924 test_name, browser, | |
925 ("Perceptual diff of reference (%s) and generated (%s) files " | |
926 "failed because: %s" % | |
927 (reference_file, generated_file, reason))) | |
928 else: | |
929 # Generated file does not exist | |
930 results.AddFailure(test_name, browser, | |
931 "File %s does not exist." % generated_file) | |
932 | |
933 return results | |
934 | 611 |
935 | 612 |
936 def main(unused_argv): | 613 def main(unused_argv): |
937 # Boolean to record if all tests passed. | 614 # Boolean to record if all tests passed. |
938 all_tests_passed = True | 615 all_tests_passed = True |
939 | 616 |
940 selenium_constants.REFERENCE_SCREENSHOT_PATH = os.path.join( | 617 selenium_constants.REFERENCE_SCREENSHOT_PATH = os.path.join( |
941 FLAGS.referencedir, | 618 FLAGS.referencedir, |
942 "reference", | 619 "reference", |
943 "") | 620 "") |
944 selenium_constants.PLATFORM_SPECIFIC_REFERENCE_SCREENSHOT_PATH = os.path.join( | 621 selenium_constants.PLATFORM_SPECIFIC_REFERENCE_SCREENSHOT_PATH = os.path.join( |
945 FLAGS.referencedir, | 622 FLAGS.referencedir, |
946 selenium_constants.PLATFORM_SCREENSHOT_DIR, | 623 selenium_constants.PLATFORM_SCREENSHOT_DIR, |
947 "") | 624 "") |
| 625 |
| 626 # Launch HTTP server. |
| 627 http_server = LocalFileHTTPServer.StartServer(FLAGS.product_dir) |
948 | 628 |
949 # Open a new session to Selenium Remote Control | 629 if not http_server: |
950 selenium_session = SeleniumSession(FLAGS.verbose, FLAGS.java, | 630 print "Could not start a local http server with root." % FLAGS.product_dir |
951 os.path.abspath(FLAGS.selenium_server), | 631 return 1 |
952 FLAGS.servertimeout) | 632 |
953 if not selenium_session.http_server or not selenium_session.selenium_server: | 633 |
| 634 # Start Selenium Remote Control and Selenium Session Builder. |
| 635 sel_server_jar = os.path.abspath(FLAGS.selenium_server) |
| 636 sel_server = SeleniumRemoteControl.StartServer( |
| 637 FLAGS.verbose, FLAGS.java, sel_server_jar, |
| 638 FLAGS.servertimeout) |
| 639 |
| 640 if not sel_server: |
| 641 print "Could not start selenium server at %s." % sel_server_jar |
954 return 1 | 642 return 1 |
955 | 643 |
| 644 session_builder = SeleniumSessionBuilder( |
| 645 sel_server.selenium_port, |
| 646 int(FLAGS.servertimeout) * 1000, |
| 647 http_server.http_port, |
| 648 FLAGS.browserpath) |
| 649 |
| 650 all_tests_passed = True |
| 651 # Test browsers. |
956 for browser in FLAGS.browser: | 652 for browser in FLAGS.browser: |
957 if browser in set(selenium_constants.SELENIUM_BROWSER_SET): | 653 if browser in set(selenium_constants.SELENIUM_BROWSER_SET): |
958 test_list = [] | 654 test_list = GetTestsForBrowser(browser, FLAGS.testprefix, |
959 result = selenium_session.TestBrowser(browser, test_list, | 655 FLAGS.testsuffixes) |
960 FLAGS.testprefix, | |
961 FLAGS.testsuffixes, | |
962 int(FLAGS.servertimeout) * 1000) | |
963 | 656 |
964 # Compare screenshots | 657 result = TestBrowser(session_builder, browser, test_list) |
965 compare_result = CompareScreenshots(browser, | 658 |
966 test_list, | 659 if not result.wasSuccessful(): |
967 FLAGS.screenshots, | |
968 FLAGS.screenshotsdir, | |
969 FLAGS.screencompare, | |
970 FLAGS.verbose) | |
971 if not result.wasSuccessful() or not compare_result.WasSuccessful(): | |
972 all_tests_passed = False | 660 all_tests_passed = False |
973 # Log results | 661 |
974 print "Results for %s:" % browser | 662 # Log non-succesful tests, for convenience. |
975 print " %d tests run." % (result.testsRun + compare_result.tests_run) | 663 print "" |
976 print " %d errors." % (len(result.errors) + len(compare_result.errors)) | 664 print "Failures for %s:" % browser |
977 print " %d failures.\n" % (len(result.failures) + | 665 print "[Selenium tests]" |
978 len(compare_result.failures)) | 666 for entry in test_list: |
| 667 if type(entry) == tuple: |
| 668 test = entry[0] |
| 669 else: |
| 670 test = entry |
| 671 |
| 672 if test in result.results: |
| 673 if result.results[test] != 'PASS': |
| 674 print test.name |
| 675 |
| 676 print "" |
| 677 print "[Perceptual Diff tests]" |
| 678 for entry in test_list: |
| 679 if type(entry) == tuple: |
| 680 pdiff_test = entry[1] |
| 681 if pdiff_test in result.results: |
| 682 if result.results[pdiff_test] != 'PASS': |
| 683 print pdiff_test.name |
| 684 |
| 685 |
| 686 # Log summary results. |
| 687 print "" |
| 688 print "Summary for %s:" % browser |
| 689 print " %d tests run." % result.testsRun |
| 690 print " %d errors." % len(result.errors) |
| 691 print " %d failures.\n" % len(result.failures) |
979 | 692 |
980 else: | 693 else: |
981 print "ERROR: Browser %s is invalid." % browser | 694 print "ERROR: Browser %s is invalid." % browser |
982 print "Run with --help to view list of supported browsers.\n" | 695 print "Run with --help to view list of supported browsers.\n" |
983 all_tests_passed = False | 696 all_tests_passed = False |
984 | 697 |
985 # Wrap up session | 698 # Shut down remote control |
986 selenium_session.TearDown() | 699 shutdown_session = selenium.selenium("localhost", |
| 700 sel_server.selenium_port, "*firefox", |
| 701 "http://%s:%d" % (socket.gethostname(), http_server.http_port)) |
| 702 shutdown_session.shut_down_selenium_server() |
987 | 703 |
988 if all_tests_passed: | 704 if all_tests_passed: |
989 # All tests successful. | 705 # All tests successful. |
990 return 0 | 706 return 0 |
991 else: | 707 else: |
992 # Return error code 1. | 708 # Return error code 1. |
993 return 1 | 709 return 1 |
994 | 710 |
995 def GetChromePath(): | |
996 value = None | |
997 if sys.platform == "win32" or sys.platform == "cygwin": | |
998 import _winreg | |
999 try: | |
1000 key = _winreg.OpenKey(_winreg.HKEY_CLASSES_ROOT, | |
1001 "Applications\\chrome.exe\\shell\\open\\command") | |
1002 (value, type) = _winreg.QueryValueEx(key, None) | |
1003 _winreg.CloseKey(key) | |
1004 except WindowsError: | |
1005 raise Exception("Unable to determine location for Chrome -- " | |
1006 "it is installed?") | |
1007 value = os.path.dirname(value) | |
1008 return value | |
1009 | |
1010 if __name__ == "__main__": | 711 if __name__ == "__main__": |
1011 remaining_argv = FLAGS(sys.argv) | 712 remaining_argv = FLAGS(sys.argv) |
1012 | 713 |
1013 # Setup the environment for Firefox | 714 # Setup the environment for Firefox |
1014 os.environ["MOZ_CRASHREPORTER_DISABLE"] = "1" | 715 os.environ["MOZ_CRASHREPORTER_DISABLE"] = "1" |
1015 os.environ["MOZ_PLUGIN_PATH"] = os.path.normpath(FLAGS.product_dir) | 716 os.environ["MOZ_PLUGIN_PATH"] = os.path.normpath(FLAGS.product_dir) |
1016 | 717 |
1017 # Setup the path for chrome. | 718 # Setup the path for chrome. |
1018 chrome_path = GetChromePath() | 719 chrome_path = GetChromePath() |
1019 if chrome_path: | 720 if chrome_path: |
1020 if os.environ.get("PATH"): | 721 if os.environ.get("PATH"): |
1021 os.environ["PATH"] = os.pathsep.join([os.environ["PATH"], chrome_path]) | 722 os.environ["PATH"] = os.pathsep.join([os.environ["PATH"], chrome_path]) |
1022 else: | 723 else: |
1023 os.environ["PATH"] = chrome_path | 724 os.environ["PATH"] = chrome_path |
1024 | 725 |
1025 # Setup the LD_LIBRARY_PATH on Linux. | 726 # Setup the LD_LIBRARY_PATH on Linux. |
1026 if sys.platform[:5] == "linux": | 727 if sys.platform[:5] == "linux": |
1027 if os.environ.get("LD_LIBRARY_PATH"): | 728 if os.environ.get("LD_LIBRARY_PATH"): |
1028 os.environ["LD_LIBRARY_PATH"] = os.pathsep.join( | 729 os.environ["LD_LIBRARY_PATH"] = os.pathsep.join( |
1029 [os.environ["LD_LIBRARY_PATH"], os.path.normpath(FLAGS.product_dir)]) | 730 [os.environ["LD_LIBRARY_PATH"], os.path.normpath(FLAGS.product_dir)]) |
1030 else: | 731 else: |
1031 os.environ["LD_LIBRARY_PATH"] = os.path.normpath(FLAGS.product_dir) | 732 os.environ["LD_LIBRARY_PATH"] = os.path.normpath(FLAGS.product_dir) |
1032 | 733 |
1033 sys.exit(main(remaining_argv)) | 734 sys.exit(main(remaining_argv)) |
OLD | NEW |