OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Generate and process code coverage. | 6 """Generate and process code coverage. |
7 | 7 |
8 TODO(jrg): rename this from coverage_posix.py to coverage_all.py! | 8 TODO(jrg): rename this from coverage_posix.py to coverage_all.py! |
9 | 9 |
10 Written for and tested on Mac, Linux, and Windows. To use this script | 10 Written for and tested on Mac, Linux, and Windows. To use this script |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
87 have all text before a ':' stripped to help with gyp compatibility. | 87 have all text before a ':' stripped to help with gyp compatibility. |
88 For example, ../base/base.gyp:base_unittests is interpreted as a test | 88 For example, ../base/base.gyp:base_unittests is interpreted as a test |
89 named "base_unittests". | 89 named "base_unittests". |
90 """ | 90 """ |
91 | 91 |
92 import glob | 92 import glob |
93 import logging | 93 import logging |
94 import optparse | 94 import optparse |
95 import os | 95 import os |
96 import Queue | 96 import Queue |
| 97 import re |
97 import shutil | 98 import shutil |
98 import signal | 99 import signal |
99 import subprocess | 100 import subprocess |
100 import sys | 101 import sys |
101 import tempfile | 102 import tempfile |
102 import threading | 103 import threading |
103 import time | 104 import time |
104 import traceback | 105 import traceback |
105 | 106 |
106 | 107 |
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
139 os.kill(pid, sig) | 140 os.kill(pid, sig) |
140 else: | 141 else: |
141 subprocess.call(['taskkill.exe', '/PID', str(pid)]) | 142 subprocess.call(['taskkill.exe', '/PID', str(pid)]) |
142 sys.exit(0) | 143 sys.exit(0) |
143 | 144 |
144 | 145 |
145 class RunTooLongException(Exception): | 146 class RunTooLongException(Exception): |
146 """Thrown when a command runs too long without output.""" | 147 """Thrown when a command runs too long without output.""" |
147 pass | 148 pass |
148 | 149 |
| 150 class BadUserInput(Exception): |
| 151 """Thrown when arguments from the user are incorrectly formatted.""" |
| 152 pass |
| 153 |
149 | 154 |
150 class RunProgramThread(threading.Thread): | 155 class RunProgramThread(threading.Thread): |
151 """A thread to run a subprocess. | 156 """A thread to run a subprocess. |
152 | 157 |
153 We want to print the output of our subprocess in real time, but also | 158 We want to print the output of our subprocess in real time, but also |
154 want a timeout if there has been no output for a certain amount of | 159 want a timeout if there has been no output for a certain amount of |
155 time. Normal techniques (e.g. loop in select()) aren't cross | 160 time. Normal techniques (e.g. loop in select()) aren't cross |
156 platform enough. the function seems simple: "print output of child, kill it | 161 platform enough. the function seems simple: "print output of child, kill it |
157 if there is no output by timeout. But it was tricky to get this right | 162 if there is no output by timeout. But it was tricky to get this right |
158 in a x-platform way (see warnings about deadlock on the python | 163 in a x-platform way (see warnings about deadlock on the python |
(...skipping 150 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
309 # The "final" lcov-format file | 314 # The "final" lcov-format file |
310 self.coverage_info_file = os.path.join(self.directory, 'coverage.info') | 315 self.coverage_info_file = os.path.join(self.directory, 'coverage.info') |
311 # If needed, an intermediate VSTS-format file | 316 # If needed, an intermediate VSTS-format file |
312 self.vsts_output = os.path.join(self.directory, 'coverage.vsts') | 317 self.vsts_output = os.path.join(self.directory, 'coverage.vsts') |
313 # Needed for Windows. | 318 # Needed for Windows. |
314 self.src_root = options.src_root | 319 self.src_root = options.src_root |
315 self.FindPrograms() | 320 self.FindPrograms() |
316 self.ConfirmPlatformAndPaths() | 321 self.ConfirmPlatformAndPaths() |
317 self.tests = [] | 322 self.tests = [] |
318 self.xvfb_pid = 0 | 323 self.xvfb_pid = 0 |
| 324 self.test_files = [] # List of files with test specifications. |
| 325 self.test_filters = {} # Mapping from testname->--gtest_filter arg. |
319 logging.info('self.directory: ' + self.directory) | 326 logging.info('self.directory: ' + self.directory) |
320 logging.info('self.directory_parent: ' + self.directory_parent) | 327 logging.info('self.directory_parent: ' + self.directory_parent) |
321 | 328 |
322 def FindInPath(self, program): | 329 def FindInPath(self, program): |
323 """Find program in our path. Return abs path to it, or None.""" | 330 """Find program in our path. Return abs path to it, or None.""" |
324 if not 'PATH' in os.environ: | 331 if not 'PATH' in os.environ: |
325 logging.fatal('No PATH environment variable?') | 332 logging.fatal('No PATH environment variable?') |
326 sys.exit(1) | 333 sys.exit(1) |
327 paths = os.environ['PATH'].split(os.pathsep) | 334 paths = os.environ['PATH'].split(os.pathsep) |
328 for path in paths: | 335 for path in paths: |
(...skipping 101 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
430 Assume all non-option items in the arg list are tests to be run. | 437 Assume all non-option items in the arg list are tests to be run. |
431 """ | 438 """ |
432 # Before we begin, find the bundles file if not an absolute path. | 439 # Before we begin, find the bundles file if not an absolute path. |
433 self.FindBundlesFile() | 440 self.FindBundlesFile() |
434 | 441 |
435 # Small tests: can be run in the "chromium" directory. | 442 # Small tests: can be run in the "chromium" directory. |
436 # If asked, run all we can find. | 443 # If asked, run all we can find. |
437 if self.options.all_unittests: | 444 if self.options.all_unittests: |
438 self.tests += glob.glob(os.path.join(self.directory, '*_unittests')) | 445 self.tests += glob.glob(os.path.join(self.directory, '*_unittests')) |
439 | 446 |
440 # Tests can come in as args or as a file of bundles. | 447 # Tests can come in as args directly, indirectly (through a file |
| 448 # of test lists) or as a file of bundles. |
441 all_testnames = self.args[:] # Copy since we might modify | 449 all_testnames = self.args[:] # Copy since we might modify |
| 450 |
| 451 for test_file in self.options.test_files: |
| 452 f = open(test_file) |
| 453 for line in f: |
| 454 line = re.sub(r"#.*$", "", line) |
| 455 line = re.sub(r"\s*", "", line) |
| 456 if re.match("\s*$"): |
| 457 continue |
| 458 all_testnames.append(line) |
| 459 f.close() |
| 460 |
442 tests_from_bundles = None | 461 tests_from_bundles = None |
443 if self.options.bundles: | 462 if self.options.bundles: |
444 try: | 463 try: |
445 tests_from_bundles = eval(open(self.options.bundles).read()) | 464 tests_from_bundles = eval(open(self.options.bundles).read()) |
446 except IOError: | 465 except IOError: |
447 logging.fatal('IO error in bundle file ' + | 466 logging.fatal('IO error in bundle file ' + |
448 self.options.bundles + ' (doesn\'t exist?)') | 467 self.options.bundles + ' (doesn\'t exist?)') |
449 except (NameError, SyntaxError): | 468 except (NameError, SyntaxError): |
450 logging.fatal('Parse or syntax error in bundle file ' + | 469 logging.fatal('Parse or syntax error in bundle file ' + |
451 self.options.bundles) | 470 self.options.bundles) |
452 if hasattr(tests_from_bundles, '__iter__'): | 471 if hasattr(tests_from_bundles, '__iter__'): |
453 all_testnames += tests_from_bundles | 472 all_testnames += tests_from_bundles |
454 else: | 473 else: |
455 logging.fatal('Fatal error with bundle file; could not get list from' + | 474 logging.fatal('Fatal error with bundle file; could not get list from' + |
456 self.options.bundles) | 475 self.options.bundles) |
457 sys.exit(1) | 476 sys.exit(1) |
458 | 477 |
459 # If told explicit tests, run those (after stripping the name as | 478 # If told explicit tests, run those (after stripping the name as |
460 # appropriate) | 479 # appropriate) |
461 for testname in all_testnames: | 480 for testname in all_testnames: |
| 481 mo = re.search(r"(.*)\[(.*)\]$", testname) |
| 482 gtest_filter = None |
| 483 if mo: |
| 484 gtest_filter = mo.group(2) |
| 485 testname = mo.group(1) |
| 486 |
462 if ':' in testname: | 487 if ':' in testname: |
463 self.tests += [os.path.join(self.directory, testname.split(':')[1])] | 488 testname = testname.split(':')[1] |
464 else: | 489 self.tests += [os.path.join(self.directory, testname)] |
465 self.tests += [os.path.join(self.directory, testname)] | 490 if gtest_filter: |
| 491 self.test_filters[testname] = gtest_filter |
| 492 |
466 # Medium tests? | 493 # Medium tests? |
467 # Not sure all of these work yet (e.g. page_cycler_tests) | 494 # Not sure all of these work yet (e.g. page_cycler_tests) |
468 # self.tests += glob.glob(os.path.join(self.directory, '*_tests')) | 495 # self.tests += glob.glob(os.path.join(self.directory, '*_tests')) |
469 | 496 |
470 # If needed, append .exe to tests since vsinstr.exe likes it that | 497 # If needed, append .exe to tests since vsinstr.exe likes it that |
471 # way. | 498 # way. |
472 if self.IsWindows(): | 499 if self.IsWindows(): |
473 for ind in range(len(self.tests)): | 500 for ind in range(len(self.tests)): |
474 test = self.tests[ind] | 501 test = self.tests[ind] |
475 test_exe = test + '.exe' | 502 test_exe = test + '.exe' |
(...skipping 117 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
593 def GtestFilter(self, fulltest, excl=None): | 620 def GtestFilter(self, fulltest, excl=None): |
594 """Return a --gtest_filter=BLAH for this test. | 621 """Return a --gtest_filter=BLAH for this test. |
595 | 622 |
596 Args: | 623 Args: |
597 fulltest: full name of test executable | 624 fulltest: full name of test executable |
598 exclusions: the exclusions list. Only set in a unit test; | 625 exclusions: the exclusions list. Only set in a unit test; |
599 else uses gTestExclusions. | 626 else uses gTestExclusions. |
600 Returns: | 627 Returns: |
601 String of the form '--gtest_filter=BLAH', or None. | 628 String of the form '--gtest_filter=BLAH', or None. |
602 """ | 629 """ |
603 if self.options.no_exclusions: | 630 positive_gfilter_list = [] |
604 return | 631 negative_gfilter_list = [] |
605 exclusions = excl or gTestExclusions | 632 |
606 excldict = exclusions.get(sys.platform) | 633 # Exclude all flaky and failing tests; they don't count for code coverage. |
607 if excldict: | 634 negative_gfilter_list += ("*.FLAKY_*", "*.FAILS_*") |
608 for test in excldict.keys(): | 635 |
609 # example: if base_unittests in ../blah/blah/base_unittests.exe | 636 if not self.options.no_exclusions: |
610 if test in fulltest: | 637 exclusions = excl or gTestExclusions |
611 gfilter = '--gtest_filter=-' + ':-'.join(excldict[test]) | 638 excldict = exclusions.get(sys.platform) |
612 return gfilter | 639 if excldict: |
613 return None | 640 for test in excldict.keys(): |
| 641 # example: if base_unittests in ../blah/blah/base_unittests.exe |
| 642 if test in fulltest: |
| 643 negative_gfilter_list += excldict[test] |
| 644 |
| 645 fulltest_basename = os.path.basename(fulltest) |
| 646 if fulltest_basename in self.test_filters: |
| 647 specific_test_filters = self.test_filters[fulltest_basename].split("-") |
| 648 if len(specific_test_filters) > 2: |
| 649 logging.error("Multiple '-' symbols in filter list: %s" % |
| 650 self.test_filters[fulltest_basename]) |
| 651 raise BadUserInput() |
| 652 if len(specific_test_filters) == 2: |
| 653 # Remove trailing ":" |
| 654 specific_test_filters[0] = specific_test_filters[0][:-1] |
| 655 |
| 656 if specific_test_filters[0]: # Test for no positive filters. |
| 657 positive_gfilter_list += specific_test_filters[0].split(":") |
| 658 if len(specific_test_filters) > 1: |
| 659 negative_gfilter_list += specific_test_filters[1].split(":") |
| 660 |
| 661 if not positive_gfilter_list and not negative_gfilter_list: |
| 662 return None |
| 663 |
| 664 result = "--gtest_filter=" |
| 665 if positive_gfilter_list: |
| 666 result += ":".join(positive_gfilter_list) |
| 667 if negative_gfilter_list: |
| 668 if positive_gfilter_list: result += ":" |
| 669 result += "-" + ":".join(negative_gfilter_list) |
| 670 return result |
614 | 671 |
615 def RunTests(self): | 672 def RunTests(self): |
616 """Run all unit tests and generate appropriate lcov files.""" | 673 """Run all unit tests and generate appropriate lcov files.""" |
617 self.BeforeRunAllTests() | 674 self.BeforeRunAllTests() |
618 for fulltest in self.tests: | 675 for fulltest in self.tests: |
619 if not os.path.exists(fulltest): | 676 if not os.path.exists(fulltest): |
620 logging.info(fulltest + ' does not exist') | 677 logging.info(fulltest + ' does not exist') |
621 if self.options.strict: | 678 if self.options.strict: |
622 sys.exit(2) | 679 sys.exit(2) |
623 else: | 680 else: |
(...skipping 257 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
881 'used for finding bundlefile (e.g. Debug)')) | 938 'used for finding bundlefile (e.g. Debug)')) |
882 parser.add_option('--no_exclusions', | 939 parser.add_option('--no_exclusions', |
883 dest='no_exclusions', | 940 dest='no_exclusions', |
884 default=None, | 941 default=None, |
885 help=('Disable the exclusion list.')) | 942 help=('Disable the exclusion list.')) |
886 parser.add_option('--dont-clear-coverage-data', | 943 parser.add_option('--dont-clear-coverage-data', |
887 dest='dont_clear_coverage_data', | 944 dest='dont_clear_coverage_data', |
888 default=False, | 945 default=False, |
889 action='store_true', | 946 action='store_true', |
890 help=('Turn off clearing of cov data from a prev run')) | 947 help=('Turn off clearing of cov data from a prev run')) |
| 948 parser.add_option('-F', |
| 949 '--test-file', |
| 950 dest="test_files", |
| 951 default=[], |
| 952 action="append", |
| 953 help=('Specify a file from which tests to be run will ' + |
| 954 'be extracted')) |
891 return parser | 955 return parser |
892 | 956 |
893 | 957 |
894 def main(): | 958 def main(): |
895 # Print out the args to help someone do it by hand if needed | 959 # Print out the args to help someone do it by hand if needed |
896 print >>sys.stderr, sys.argv | 960 print >>sys.stderr, sys.argv |
897 | 961 |
898 # Try and clean up nice if we're killed by buildbot, Ctrl-C, ... | 962 # Try and clean up nice if we're killed by buildbot, Ctrl-C, ... |
899 signal.signal(signal.SIGINT, TerminateSignalHandler) | 963 signal.signal(signal.SIGINT, TerminateSignalHandler) |
900 signal.signal(signal.SIGTERM, TerminateSignalHandler) | 964 signal.signal(signal.SIGTERM, TerminateSignalHandler) |
901 | 965 |
902 parser = CoverageOptionParser() | 966 parser = CoverageOptionParser() |
903 (options, args) = parser.parse_args() | 967 (options, args) = parser.parse_args() |
904 coverage = Coverage(options, args) | 968 coverage = Coverage(options, args) |
905 coverage.ClearData() | 969 coverage.ClearData() |
906 coverage.FindTests() | 970 coverage.FindTests() |
907 if options.trim: | 971 if options.trim: |
908 coverage.TrimTests() | 972 coverage.TrimTests() |
909 coverage.RunTests() | 973 coverage.RunTests() |
910 if options.genhtml: | 974 if options.genhtml: |
911 coverage.GenerateHtml() | 975 coverage.GenerateHtml() |
912 return 0 | 976 return 0 |
913 | 977 |
914 | 978 |
915 if __name__ == '__main__': | 979 if __name__ == '__main__': |
916 sys.exit(main()) | 980 sys.exit(main()) |
OLD | NEW |