OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright 2014 the V8 project authors. All rights reserved. | 2 # Copyright 2014 the V8 project 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 """ | 6 """ |
7 Performance runner for d8. | 7 Performance runner for d8. |
8 | 8 |
9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json | 9 Call e.g. with tools/run-perf.py --arch ia32 some_suite.json |
10 | 10 |
(...skipping 387 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
398 self.timeout = suite.get("timeout_%s" % arch, self.timeout) | 398 self.timeout = suite.get("timeout_%s" % arch, self.timeout) |
399 self.units = suite.get("units", parent.units) | 399 self.units = suite.get("units", parent.units) |
400 self.total = suite.get("total", parent.total) | 400 self.total = suite.get("total", parent.total) |
401 | 401 |
402 # A regular expression for results. If the parent graph provides a | 402 # A regular expression for results. If the parent graph provides a |
403 # regexp and the current suite has none, a string place holder for the | 403 # regexp and the current suite has none, a string place holder for the |
404 # suite name is expected. | 404 # suite name is expected. |
405 # TODO(machenbach): Currently that makes only sense for the leaf level. | 405 # TODO(machenbach): Currently that makes only sense for the leaf level. |
406 # Multiple place holders for multiple levels are not supported. | 406 # Multiple place holders for multiple levels are not supported. |
407 if parent.results_regexp: | 407 if parent.results_regexp: |
408 regexp_default = parent.results_regexp % re.escape(suite["name"]) | 408 try: |
409 regexp_default = parent.results_regexp % re.escape(suite["name"]) | |
410 except TypeError: | |
411 regexp_default = parent.results_regexp | |
409 else: | 412 else: |
410 regexp_default = None | 413 regexp_default = None |
411 self.results_regexp = suite.get("results_regexp", regexp_default) | 414 self.results_regexp = suite.get("results_regexp", regexp_default) |
412 | 415 |
413 # A similar regular expression for the standard deviation (optional). | 416 # A similar regular expression for the standard deviation (optional). |
414 if parent.stddev_regexp: | 417 if parent.stddev_regexp: |
415 stddev_default = parent.stddev_regexp % re.escape(suite["name"]) | 418 stddev_default = parent.stddev_regexp % re.escape(suite["name"]) |
416 else: | 419 else: |
417 stddev_default = None | 420 stddev_default = None |
418 self.stddev_regexp = suite.get("stddev_regexp", stddev_default) | 421 self.stddev_regexp = suite.get("stddev_regexp", stddev_default) |
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
580 self.shell_dir_no_patch = options.shell_dir_no_patch | 583 self.shell_dir_no_patch = options.shell_dir_no_patch |
581 self.extra_flags = options.extra_flags.split() | 584 self.extra_flags = options.extra_flags.split() |
582 | 585 |
583 @staticmethod | 586 @staticmethod |
584 def GetPlatform(options): | 587 def GetPlatform(options): |
585 if options.android_build_tools: | 588 if options.android_build_tools: |
586 return AndroidPlatform(options) | 589 return AndroidPlatform(options) |
587 else: | 590 else: |
588 return DesktopPlatform(options) | 591 return DesktopPlatform(options) |
589 | 592 |
593 def GetPrettyFormatted(self, options): | |
594 return self | |
595 | |
596 def PreExecution(self): | |
597 pass | |
598 | |
599 def PostExecution(self): | |
600 pass | |
601 | |
602 def PreTests(self, node, path): | |
603 pass | |
604 | |
605 def PrintResult(self, result): | |
606 pass | |
607 | |
608 def _PrintStdout(self, title, output): | |
609 print title % "Stdout" | |
610 print output.stdout | |
611 | |
590 def _Run(self, runnable, count, no_patch=False): | 612 def _Run(self, runnable, count, no_patch=False): |
591 raise NotImplementedError() # pragma: no cover | 613 raise NotImplementedError() # pragma: no cover |
592 | 614 |
593 def Run(self, runnable, count): | 615 def Run(self, runnable, count): |
594 """Execute the benchmark's main file. | 616 """Execute the benchmark's main file. |
595 | 617 |
596 If options.shell_dir_no_patch is specified, the benchmark is run once with | 618 If options.shell_dir_no_patch is specified, the benchmark is run once with |
597 and once without patch. | 619 and once without patch. |
598 Args: | 620 Args: |
599 runnable: A Runnable benchmark instance. | 621 runnable: A Runnable benchmark instance. |
600 count: The number of this (repeated) run. | 622 count: The number of this (repeated) run. |
601 Returns: A tuple with the benchmark outputs with and without patch. The | 623 Returns: A tuple with the benchmark outputs with and without patch. The |
602 latter will be None if options.shell_dir_no_patch was not | 624 latter will be None if options.shell_dir_no_patch was not |
603 specified. | 625 specified. |
604 """ | 626 """ |
605 stdout = self._Run(runnable, count, no_patch=False) | 627 stdout = self._Run(runnable, count, no_patch=False) |
606 if self.shell_dir_no_patch: | 628 if self.shell_dir_no_patch: |
607 return stdout, self._Run(runnable, count, no_patch=True) | 629 return stdout, self._Run(runnable, count, no_patch=True) |
608 else: | 630 else: |
609 return stdout, None | 631 return stdout, None |
610 | 632 |
611 | 633 |
634 class PlatformFormattedMixin(object): | |
635 """ | |
636 Helper mixin that adds formatted output used when running benchmarks | |
637 with the --pretty flag. | |
638 """ | |
639 | |
640 def _PrintStdout(self, title, output): | |
641 sys.stdout.write("\r") | |
642 if output.exit_code != 0: | |
643 print output.stdout | |
644 return | |
645 # Assume the time is on the last line | |
646 result_line = output.stdout.splitlines()[-1].strip() | |
647 sys.stdout.write(result_line) | |
648 # Fill with spaces up to 80 characters. | |
649 sys.stdout.write(' '*max(0, 80-len(result_line))) | |
650 sys.stdout.flush() | |
651 | |
652 def _GetMean(self, trace): | |
653 results = trace['results'] | |
654 if len(results) == 0: | |
655 return 0 | |
656 # If the tests provided a stddev the results consists of one single average | |
657 # value, so return that instead. | |
658 if trace['stddev']: | |
659 return results[0] | |
660 # For a non-zero length results list calculate the average here. | |
661 return sum([float(x) for x in results]) / len(results) | |
662 | |
663 def _GetDeviation(self, trace): | |
664 # If the benchmark provided a stddev use that directly. | |
665 stddev = trace['stddev'] | |
666 if stddev: | |
667 return stddev | |
668 # If no stddev was provided calculate it from the results. | |
669 results = trace['results'] | |
670 if len(results) == 0: | |
671 return 0 | |
672 mean = self._GetMean(trace) | |
673 square_deviation = sum((float(x)-mean)**2 for x in results) | |
674 return (square_deviation / len(results)) ** 0.5 | |
675 | |
676 def PrintResult(self, result): | |
677 if result.errors: | |
678 print "\r:Errors:" | |
679 print "\n".join(set(result.errors)) | |
680 else: | |
681 trace = result.traces[0] | |
682 average = self._GetMean(trace) | |
683 stdev = self._GetDeviation(trace) | |
684 stdev_percentage = 100 * stdev / average if average != 0 else 0 | |
685 result_string = "\r %s +/- %3.2f%% %s" % ( | |
686 average, stdev_percentage, trace['units']) | |
687 sys.stdout.write(result_string) | |
688 # Fill with spaces up to 80 characters. | |
689 sys.stdout.write(' '*max(0, 80-len(result_string))) | |
690 sys.stdout.write("\n") | |
691 sys.stdout.flush() | |
692 | |
693 | |
612 class DesktopPlatform(Platform): | 694 class DesktopPlatform(Platform): |
613 def __init__(self, options): | 695 def __init__(self, options): |
614 super(DesktopPlatform, self).__init__(options) | 696 super(DesktopPlatform, self).__init__(options) |
615 | 697 |
616 def PreExecution(self): | 698 def GetPrettyFormatted(self, options): |
617 pass | 699 return PrettyFormattedDesktopPlatform(options) |
618 | |
619 def PostExecution(self): | |
620 pass | |
621 | 700 |
622 def PreTests(self, node, path): | 701 def PreTests(self, node, path): |
623 if isinstance(node, RunnableConfig): | 702 if isinstance(node, RunnableConfig): |
624 node.ChangeCWD(path) | 703 node.ChangeCWD(path) |
625 | 704 |
626 def _Run(self, runnable, count, no_patch=False): | 705 def _Run(self, runnable, count, no_patch=False): |
627 suffix = ' - without patch' if no_patch else '' | 706 suffix = ' - without patch' if no_patch else '' |
628 shell_dir = self.shell_dir_no_patch if no_patch else self.shell_dir | 707 shell_dir = self.shell_dir_no_patch if no_patch else self.shell_dir |
629 title = ">>> %%s (#%d)%s:" % ((count + 1), suffix) | 708 title = ">>> %%s (#%d)%s:" % ((count + 1), suffix) |
630 try: | 709 try: |
631 output = commands.Execute( | 710 output = commands.Execute( |
632 runnable.GetCommand(shell_dir, self.extra_flags), | 711 runnable.GetCommand(shell_dir, self.extra_flags), |
633 timeout=runnable.timeout, | 712 timeout=runnable.timeout, |
634 ) | 713 ) |
635 except OSError as e: # pragma: no cover | 714 except OSError as e: # pragma: no cover |
636 print title % "OSError" | 715 print title % "OSError" |
637 print e | 716 print e |
638 return "" | 717 return "" |
639 print title % "Stdout" | 718 self._PrintStdout(title, output) |
640 print output.stdout | |
641 if output.stderr: # pragma: no cover | 719 if output.stderr: # pragma: no cover |
642 # Print stderr for debugging. | 720 # Print stderr for debugging. |
643 print title % "Stderr" | 721 print title % "Stderr" |
644 print output.stderr | 722 print output.stderr |
645 if output.timed_out: | 723 if output.timed_out: |
646 print ">>> Test timed out after %ss." % runnable.timeout | 724 print ">>> Test timed out after %ss." % runnable.timeout |
647 if '--prof' in self.extra_flags: | 725 if '--prof' in self.extra_flags: |
648 os_prefix = {"linux": "linux", "macos": "mac"}.get(utils.GuessOS()) | 726 os_prefix = {"linux": "linux", "macos": "mac"}.get(utils.GuessOS()) |
649 if os_prefix: | 727 if os_prefix: |
650 tick_tools = os.path.join(TOOLS_BASE, "%s-tick-processor" % os_prefix) | 728 tick_tools = os.path.join(TOOLS_BASE, "%s-tick-processor" % os_prefix) |
651 subprocess.check_call(tick_tools + " --only-summary", shell=True) | 729 subprocess.check_call(tick_tools + " --only-summary", shell=True) |
652 else: # pragma: no cover | 730 else: # pragma: no cover |
653 print "Profiler option currently supported on Linux and Mac OS." | 731 print "Profiler option currently supported on Linux and Mac OS." |
654 return output.stdout | 732 return output.stdout |
655 | 733 |
656 | 734 |
735 class PrettyFormattedDesktopPlatform(PlatformFormattedMixin, DesktopPlatform): | |
736 pass | |
737 | |
738 | |
657 class AndroidPlatform(Platform): # pragma: no cover | 739 class AndroidPlatform(Platform): # pragma: no cover |
658 DEVICE_DIR = "/data/local/tmp/v8/" | 740 DEVICE_DIR = "/data/local/tmp/v8/" |
659 | 741 |
660 def __init__(self, options): | 742 def __init__(self, options): |
661 super(AndroidPlatform, self).__init__(options) | 743 super(AndroidPlatform, self).__init__(options) |
662 LoadAndroidBuildTools(options.android_build_tools) | 744 LoadAndroidBuildTools(options.android_build_tools) |
663 | 745 |
664 if not options.device: | 746 if not options.device: |
665 # Detect attached device if not specified. | 747 # Detect attached device if not specified. |
666 devices = adb_wrapper.AdbWrapper.Devices() | 748 devices = adb_wrapper.AdbWrapper.Devices() |
667 assert devices and len(devices) == 1, ( | 749 assert devices and len(devices) == 1, ( |
668 "None or multiple devices detected. Please specify the device on " | 750 "None or multiple devices detected. Please specify the device on " |
669 "the command-line with --device") | 751 "the command-line with --device") |
670 options.device = str(devices[0]) | 752 options.device = str(devices[0]) |
671 self.adb_wrapper = adb_wrapper.AdbWrapper(options.device) | 753 self.adb_wrapper = adb_wrapper.AdbWrapper(options.device) |
672 self.device = device_utils.DeviceUtils(self.adb_wrapper) | 754 self.device = device_utils.DeviceUtils(self.adb_wrapper) |
673 | 755 |
756 def GetPrettyFormatted(self, options): | |
757 return PrettyFormattedAndroidPlatform(options) | |
758 | |
674 def PreExecution(self): | 759 def PreExecution(self): |
675 perf = perf_control.PerfControl(self.device) | 760 perf = perf_control.PerfControl(self.device) |
676 perf.SetHighPerfMode() | 761 perf.SetHighPerfMode() |
677 | 762 |
678 # Remember what we have already pushed to the device. | 763 # Remember what we have already pushed to the device. |
679 self.pushed = set() | 764 self.pushed = set() |
680 | 765 |
681 def PostExecution(self): | 766 def PostExecution(self): |
682 perf = perf_control.PerfControl(self.device) | 767 perf = perf_control.PerfControl(self.device) |
683 perf.SetDefaultPerfMode() | 768 perf.SetDefaultPerfMode() |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
750 self._PushExecutable(self.shell_dir, "bin", node.binary) | 835 self._PushExecutable(self.shell_dir, "bin", node.binary) |
751 if self.shell_dir_no_patch: | 836 if self.shell_dir_no_patch: |
752 self._PushExecutable( | 837 self._PushExecutable( |
753 self.shell_dir_no_patch, "bin_no_patch", node.binary) | 838 self.shell_dir_no_patch, "bin_no_patch", node.binary) |
754 | 839 |
755 if isinstance(node, RunnableConfig): | 840 if isinstance(node, RunnableConfig): |
756 self._PushFile(bench_abs, node.main, bench_rel) | 841 self._PushFile(bench_abs, node.main, bench_rel) |
757 for resource in node.resources: | 842 for resource in node.resources: |
758 self._PushFile(bench_abs, resource, bench_rel) | 843 self._PushFile(bench_abs, resource, bench_rel) |
759 | 844 |
845 def _PrintStdout(self, title, output): | |
846 print title % "Stdout" | |
847 print "\n".join(output) | |
848 | |
760 def _Run(self, runnable, count, no_patch=False): | 849 def _Run(self, runnable, count, no_patch=False): |
761 suffix = ' - without patch' if no_patch else '' | 850 suffix = ' - without patch' if no_patch else '' |
762 target_dir = "bin_no_patch" if no_patch else "bin" | 851 target_dir = "bin_no_patch" if no_patch else "bin" |
763 title = ">>> %%s (#%d)%s:" % ((count + 1), suffix) | 852 title = ">>> %%s (#%d)%s:" % ((count + 1), suffix) |
764 cache = cache_control.CacheControl(self.device) | 853 cache = cache_control.CacheControl(self.device) |
765 cache.DropRamCaches() | 854 cache.DropRamCaches() |
766 binary_on_device = os.path.join( | 855 binary_on_device = os.path.join( |
767 AndroidPlatform.DEVICE_DIR, target_dir, runnable.binary) | 856 AndroidPlatform.DEVICE_DIR, target_dir, runnable.binary) |
768 cmd = [binary_on_device] + runnable.GetCommandFlags(self.extra_flags) | 857 cmd = [binary_on_device] + runnable.GetCommandFlags(self.extra_flags) |
769 | 858 |
770 # Relative path to benchmark directory. | 859 # Relative path to benchmark directory. |
771 if runnable.path: | 860 if runnable.path: |
772 bench_rel = os.path.normpath(os.path.join(*runnable.path)) | 861 bench_rel = os.path.normpath(os.path.join(*runnable.path)) |
773 else: | 862 else: |
774 bench_rel = "." | 863 bench_rel = "." |
775 | 864 |
776 try: | 865 try: |
777 output = self.device.RunShellCommand( | 866 output = self.device.RunShellCommand( |
778 cmd, | 867 cmd, |
779 cwd=os.path.join(AndroidPlatform.DEVICE_DIR, bench_rel), | 868 cwd=os.path.join(AndroidPlatform.DEVICE_DIR, bench_rel), |
780 timeout=runnable.timeout, | 869 timeout=runnable.timeout, |
781 retries=0, | 870 retries=0, |
782 ) | 871 ) |
783 stdout = "\n".join(output) | 872 self._PrintStdout(title, output) |
784 print title % "Stdout" | |
785 print stdout | |
786 except device_errors.CommandTimeoutError: | 873 except device_errors.CommandTimeoutError: |
787 print ">>> Test timed out after %ss." % runnable.timeout | 874 print ">>> Test timed out after %ss." % runnable.timeout |
788 stdout = "" | 875 stdout = "" |
789 return stdout | 876 return stdout |
Michael Achenbach
2016/02/16 13:01:49
Meh. This needs to contain the stdout from above.
| |
790 | 877 |
791 | 878 |
879 class PrettyFormattedAndroidPlatform(PlatformFormattedMixin, AndroidPlatform): | |
880 pass | |
881 | |
882 | |
792 # TODO: Implement results_processor. | 883 # TODO: Implement results_processor. |
793 def Main(args): | 884 def Main(args): |
794 logging.getLogger().setLevel(logging.INFO) | 885 logging.getLogger().setLevel(logging.INFO) |
795 parser = optparse.OptionParser() | 886 parser = optparse.OptionParser() |
796 parser.add_option("--android-build-tools", | 887 parser.add_option("--android-build-tools", |
797 help="Path to chromium's build/android. Specifying this " | 888 help="Path to chromium's build/android. Specifying this " |
798 "option will run tests using android platform.") | 889 "option will run tests using android platform.") |
799 parser.add_option("--arch", | 890 parser.add_option("--arch", |
800 help=("The architecture to run tests for, " | 891 help=("The architecture to run tests for, " |
801 "'auto' or 'native' for auto-detect"), | 892 "'auto' or 'native' for auto-detect"), |
802 default="x64") | 893 default="x64") |
803 parser.add_option("--buildbot", | 894 parser.add_option("--buildbot", |
804 help="Adapt to path structure used on buildbots", | 895 help="Adapt to path structure used on buildbots", |
805 default=False, action="store_true") | 896 default=False, action="store_true") |
806 parser.add_option("--device", | 897 parser.add_option("--device", |
807 help="The device ID to run Android tests on. If not given " | 898 help="The device ID to run Android tests on. If not given " |
808 "it will be autodetected.") | 899 "it will be autodetected.") |
809 parser.add_option("--extra-flags", | 900 parser.add_option("--extra-flags", |
810 help="Additional flags to pass to the test executable", | 901 help="Additional flags to pass to the test executable", |
811 default="") | 902 default="") |
812 parser.add_option("--json-test-results", | 903 parser.add_option("--json-test-results", |
813 help="Path to a file for storing json results.") | 904 help="Path to a file for storing json results.") |
814 parser.add_option("--json-test-results-no-patch", | 905 parser.add_option("--json-test-results-no-patch", |
815 help="Path to a file for storing json results from run " | 906 help="Path to a file for storing json results from run " |
816 "without patch.") | 907 "without patch.") |
817 parser.add_option("--outdir", help="Base directory with compile output", | 908 parser.add_option("--outdir", help="Base directory with compile output", |
818 default="out") | 909 default="out") |
819 parser.add_option("--outdir-no-patch", | 910 parser.add_option("--outdir-no-patch", |
820 help="Base directory with compile output without patch") | 911 help="Base directory with compile output without patch") |
912 parser.add_option("--pretty", | |
913 help="Print human readable output", | |
914 default=False, action="store_true") | |
821 parser.add_option("--binary-override-path", | 915 parser.add_option("--binary-override-path", |
822 help="JavaScript engine binary. By default, d8 under " | 916 help="JavaScript engine binary. By default, d8 under " |
823 "architecture-specific build dir. " | 917 "architecture-specific build dir. " |
824 "Not supported in conjunction with outdir-no-patch.") | 918 "Not supported in conjunction with outdir-no-patch.") |
825 | 919 |
826 (options, args) = parser.parse_args(args) | 920 (options, args) = parser.parse_args(args) |
827 | 921 |
828 if len(args) == 0: # pragma: no cover | 922 if len(args) == 0: # pragma: no cover |
829 parser.print_help() | 923 parser.print_help() |
830 return 1 | 924 return 1 |
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
866 options.shell_dir = os.path.dirname(options.binary_override_path) | 960 options.shell_dir = os.path.dirname(options.binary_override_path) |
867 default_binary_name = os.path.basename(options.binary_override_path) | 961 default_binary_name = os.path.basename(options.binary_override_path) |
868 | 962 |
869 if options.outdir_no_patch: | 963 if options.outdir_no_patch: |
870 options.shell_dir_no_patch = os.path.join( | 964 options.shell_dir_no_patch = os.path.join( |
871 workspace, options.outdir_no_patch, build_config) | 965 workspace, options.outdir_no_patch, build_config) |
872 else: | 966 else: |
873 options.shell_dir_no_patch = None | 967 options.shell_dir_no_patch = None |
874 | 968 |
875 platform = Platform.GetPlatform(options) | 969 platform = Platform.GetPlatform(options) |
970 if options.pretty: | |
971 platform = platform.GetPrettyFormatted(options) | |
876 | 972 |
877 results = Results() | 973 results = Results() |
878 results_no_patch = Results() | 974 results_no_patch = Results() |
879 for path in args: | 975 for path in args: |
880 path = os.path.abspath(path) | 976 path = os.path.abspath(path) |
881 | 977 |
882 if not os.path.exists(path): # pragma: no cover | 978 if not os.path.exists(path): # pragma: no cover |
883 results.errors.append("Configuration file %s does not exist." % path) | 979 results.errors.append("Configuration file %s does not exist." % path) |
884 continue | 980 continue |
885 | 981 |
(...skipping 21 matching lines...) Expand all Loading... | |
907 def Runner(): | 1003 def Runner(): |
908 """Output generator that reruns several times.""" | 1004 """Output generator that reruns several times.""" |
909 for i in xrange(0, max(1, runnable.run_count)): | 1005 for i in xrange(0, max(1, runnable.run_count)): |
910 # TODO(machenbach): Allow timeout per arch like with run_count per | 1006 # TODO(machenbach): Allow timeout per arch like with run_count per |
911 # arch. | 1007 # arch. |
912 yield platform.Run(runnable, i) | 1008 yield platform.Run(runnable, i) |
913 | 1009 |
914 # Let runnable iterate over all runs and handle output. | 1010 # Let runnable iterate over all runs and handle output. |
915 result, result_no_patch = runnable.Run( | 1011 result, result_no_patch = runnable.Run( |
916 Runner, trybot=options.shell_dir_no_patch) | 1012 Runner, trybot=options.shell_dir_no_patch) |
1013 platform.PrintResult(result) | |
917 results += result | 1014 results += result |
918 results_no_patch += result_no_patch | 1015 results_no_patch += result_no_patch |
919 platform.PostExecution() | 1016 platform.PostExecution() |
920 | 1017 |
921 if options.json_test_results: | 1018 if options.json_test_results: |
922 results.WriteToFile(options.json_test_results) | 1019 results.WriteToFile(options.json_test_results) |
923 else: # pragma: no cover | 1020 else: # pragma: no cover |
924 print results | 1021 if not options.pretty: |
1022 print results | |
925 | 1023 |
926 if options.json_test_results_no_patch: | 1024 if options.json_test_results_no_patch: |
927 results_no_patch.WriteToFile(options.json_test_results_no_patch) | 1025 results_no_patch.WriteToFile(options.json_test_results_no_patch) |
928 else: # pragma: no cover | 1026 else: # pragma: no cover |
929 print results_no_patch | 1027 if not options.pretty: |
1028 print results_no_patch | |
930 | 1029 |
931 return min(1, len(results.errors)) | 1030 return min(1, len(results.errors)) |
932 | 1031 |
933 if __name__ == "__main__": # pragma: no cover | 1032 if __name__ == "__main__": # pragma: no cover |
934 sys.exit(Main(sys.argv[1:])) | 1033 sys.exit(Main(sys.argv[1:])) |
OLD | NEW |