| #!/usr/bin/env python |
| # Copyright 2017 The PDFium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Compares the performance of two versions of the pdfium code.""" |
| |
| import argparse |
| import functools |
| import glob |
| import json |
| import multiprocessing |
| import os |
| import re |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| |
| from common import GetBooleanGnArg |
| from common import PrintErr |
| from common import RunCommandPropagateErr |
| from githelper import GitHelper |
| from safetynet_conclusions import ComparisonConclusions |
| from safetynet_conclusions import PrintConclusionsDictHumanReadable |
| from safetynet_conclusions import RATING_IMPROVEMENT |
| from safetynet_conclusions import RATING_REGRESSION |
| from safetynet_image import ImageComparison |
| |
| |
| def RunSingleTestCaseParallel(this, run_label, build_dir, test_case): |
| result = this.RunSingleTestCase(run_label, build_dir, test_case) |
| return (test_case, result) |
| |
| |
| class CompareRun(object): |
| """A comparison between two branches of pdfium.""" |
| |
| def __init__(self, args): |
| self.git = GitHelper() |
| self.args = args |
| self._InitPaths() |
| |
| def _InitPaths(self): |
| if self.args.this_repo: |
| self.safe_script_dir = self.args.build_dir |
| else: |
| self.safe_script_dir = os.path.join('testing', 'tools') |
| |
| self.safe_measure_script_path = os.path.abspath( |
| os.path.join(self.safe_script_dir, |
| 'safetynet_measure.py')) |
| |
| input_file_re = re.compile('^.+[.]pdf$') |
| self.test_cases = [] |
| for input_path in self.args.input_paths: |
| if os.path.isfile(input_path): |
| self.test_cases.append(input_path) |
| elif os.path.isdir(input_path): |
| for file_dir, _, filename_list in os.walk(input_path): |
| for input_filename in filename_list: |
| if input_file_re.match(input_filename): |
| file_path = os.path.join(file_dir, input_filename) |
| if os.path.isfile(file_path): |
| self.test_cases.append(file_path) |
| |
| self.after_build_dir = self.args.build_dir |
| if self.args.build_dir_before: |
| self.before_build_dir = self.args.build_dir_before |
| else: |
| self.before_build_dir = self.after_build_dir |
| |
| def Run(self): |
| """Runs comparison by checking out branches, building and measuring them. |
| |
| Returns: |
| Exit code for the script. |
| """ |
| if self.args.this_repo: |
| self._FreezeMeasureScript() |
| |
| if self.args.branch_after: |
| if self.args.this_repo: |
| before, after = self._ProfileTwoOtherBranchesInThisRepo( |
| self.args.branch_before, |
| self.args.branch_after) |
| else: |
| before, after = self._ProfileTwoOtherBranches( |
| self.args.branch_before, |
| self.args.branch_after) |
| elif self.args.branch_before: |
| if self.args.this_repo: |
| before, after = self._ProfileCurrentAndOtherBranchInThisRepo( |
| self.args.branch_before) |
| else: |
| before, after = self._ProfileCurrentAndOtherBranch( |
| self.args.branch_before) |
| else: |
| if self.args.this_repo: |
| before, after = self._ProfileLocalChangesAndCurrentBranchInThisRepo() |
| else: |
| before, after = self._ProfileLocalChangesAndCurrentBranch() |
| |
| conclusions = self._DrawConclusions(before, after) |
| conclusions_dict = conclusions.GetOutputDict() |
| conclusions_dict.setdefault('metadata', {})['profiler'] = self.args.profiler |
| |
| self._PrintConclusions(conclusions_dict) |
| |
| self._CleanUp(conclusions) |
| |
| if self.args.png_dir: |
| image_comparison = ImageComparison( |
| self.after_build_dir, |
| self.args.png_dir, |
| ('before', 'after'), |
| self.args.num_workers, |
| self.args.png_threshold) |
| image_comparison.Run(open_in_browser=not self.args.machine_readable) |
| |
| return 0 |
| |
| def _FreezeMeasureScript(self): |
| """Freezes a version of the measuring script. |
| |
| This is needed to make sure we are comparing the pdfium library changes and |
| not script changes that may happen between the two branches. |
| """ |
| self.__FreezeFile(os.path.join('testing', 'tools', 'safetynet_measure.py')) |
| self.__FreezeFile(os.path.join('testing', 'tools', 'common.py')) |
| |
| def __FreezeFile(self, file): |
| RunCommandPropagateErr(['cp', file, self.safe_script_dir], |
| exit_status_on_error=1) |
| |
| def _ProfileTwoOtherBranchesInThisRepo(self, before_branch, after_branch): |
| """Profiles two branches that are not the current branch. |
| |
| This is done in the local repository and changes may not be restored if the |
| script fails or is interrupted. |
| |
| after_branch does not need to descend from before_branch, they will be |
| measured the same way |
| |
| Args: |
| before_branch: One branch to profile. |
| after_branch: Other branch to profile. |
| |
| Returns: |
| A tuple (before, after), where each of before and after is a dict |
| mapping a test case name to the profiling values for that test case |
| in the given branch. |
| """ |
| branch_to_restore = self.git.GetCurrentBranchName() |
| |
| self._StashLocalChanges() |
| |
| self._CheckoutBranch(after_branch) |
| self._BuildCurrentBranch(self.after_build_dir) |
| after = self._MeasureCurrentBranch('after', self.after_build_dir) |
| |
| self._CheckoutBranch(before_branch) |
| self._BuildCurrentBranch(self.before_build_dir) |
| before = self._MeasureCurrentBranch('before', self.before_build_dir) |
| |
| self._CheckoutBranch(branch_to_restore) |
| self._RestoreLocalChanges() |
| |
| return before, after |
| |
| def _ProfileTwoOtherBranches(self, before_branch, after_branch): |
| """Profiles two branches that are not the current branch. |
| |
| This is done in new, cloned repositories, therefore it is safer but slower |
| and requires downloads. |
| |
| after_branch does not need to descend from before_branch, they will be |
| measured the same way |
| |
| Args: |
| before_branch: One branch to profile. |
| after_branch: Other branch to profile. |
| |
| Returns: |
| A tuple (before, after), where each of before and after is a dict |
| mapping a test case name to the profiling values for that test case |
| in the given branch. |
| """ |
| after = self._ProfileSeparateRepo('after', |
| self.after_build_dir, |
| after_branch) |
| before = self._ProfileSeparateRepo('before', |
| self.before_build_dir, |
| before_branch) |
| return before, after |
| |
| def _ProfileCurrentAndOtherBranchInThisRepo(self, other_branch): |
| """Profiles the current branch (with uncommitted changes) and another one. |
| |
| This is done in the local repository and changes may not be restored if the |
| script fails or is interrupted. |
| |
| The current branch does not need to descend from other_branch. |
| |
| Args: |
| other_branch: Other branch to profile that is not the current. |
| |
| Returns: |
| A tuple (before, after), where each of before and after is a dict |
| mapping a test case name to the profiling values for that test case |
| in the given branch. The current branch is considered to be "after" and |
| the other branch is considered to be "before". |
| """ |
| branch_to_restore = self.git.GetCurrentBranchName() |
| |
| self._BuildCurrentBranch(self.after_build_dir) |
| after = self._MeasureCurrentBranch('after', self.after_build_dir) |
| |
| self._StashLocalChanges() |
| |
| self._CheckoutBranch(other_branch) |
| self._BuildCurrentBranch(self.before_build_dir) |
| before = self._MeasureCurrentBranch('before', self.before_build_dir) |
| |
| self._CheckoutBranch(branch_to_restore) |
| self._RestoreLocalChanges() |
| |
| return before, after |
| |
| def _ProfileCurrentAndOtherBranch(self, other_branch): |
| """Profiles the current branch (with uncommitted changes) and another one. |
| |
| This is done in new, cloned repositories, therefore it is safer but slower |
| and requires downloads. |
| |
| The current branch does not need to descend from other_branch. |
| |
| Args: |
| other_branch: Other branch to profile that is not the current. None will |
| compare to the same branch. |
| |
| Returns: |
| A tuple (before, after), where each of before and after is a dict |
| mapping a test case name to the profiling values for that test case |
| in the given branch. The current branch is considered to be "after" and |
| the other branch is considered to be "before". |
| """ |
| self._BuildCurrentBranch(self.after_build_dir) |
| after = self._MeasureCurrentBranch('after', self.after_build_dir) |
| |
| before = self._ProfileSeparateRepo('before', |
| self.before_build_dir, |
| other_branch) |
| |
| return before, after |
| |
| def _ProfileLocalChangesAndCurrentBranchInThisRepo(self): |
| """Profiles the current branch with and without uncommitted changes. |
| |
| This is done in the local repository and changes may not be restored if the |
| script fails or is interrupted. |
| |
| Returns: |
| A tuple (before, after), where each of before and after is a dict |
| mapping a test case name to the profiling values for that test case |
| using the given version. The current branch without uncommitted changes is |
| considered to be "before" and with uncommitted changes is considered to be |
| "after". |
| """ |
| self._BuildCurrentBranch(self.after_build_dir) |
| after = self._MeasureCurrentBranch('after', self.after_build_dir) |
| |
| pushed = self._StashLocalChanges() |
| if not pushed and not self.args.build_dir_before: |
| PrintErr('Warning: No local changes to compare') |
| |
| before_build_dir = self.before_build_dir |
| |
| self._BuildCurrentBranch(before_build_dir) |
| before = self._MeasureCurrentBranch('before', before_build_dir) |
| |
| self._RestoreLocalChanges() |
| |
| return before, after |
| |
| def _ProfileLocalChangesAndCurrentBranch(self): |
| """Profiles the current branch with and without uncommitted changes. |
| |
| This is done in new, cloned repositories, therefore it is safer but slower |
| and requires downloads. |
| |
| Returns: |
| A tuple (before, after), where each of before and after is a dict |
| mapping a test case name to the profiling values for that test case |
| using the given version. The current branch without uncommitted changes is |
| considered to be "before" and with uncommitted changes is considered to be |
| "after". |
| """ |
| return self._ProfileCurrentAndOtherBranch(other_branch=None) |
| |
| def _ProfileSeparateRepo(self, run_label, relative_build_dir, branch): |
| """Profiles a branch in a a temporary git repository. |
| |
| Args: |
| run_label: String to differentiate this version of the code in output |
| files from other versions. |
| relative_build_dir: Path to the build dir in the current working dir to |
| clone build args from. |
| branch: Branch to checkout in the new repository. None will |
| profile the same branch checked out in the original repo. |
| Returns: |
| A dict mapping each test case name to the profiling values for that |
| test case. |
| """ |
| build_dir = self._CreateTempRepo('repo_%s' % run_label, |
| relative_build_dir, |
| branch) |
| |
| self._BuildCurrentBranch(build_dir) |
| return self._MeasureCurrentBranch(run_label, build_dir) |
| |
| def _CreateTempRepo(self, dir_name, relative_build_dir, branch): |
| """Clones a temporary git repository out of the current working dir. |
| |
| Args: |
| dir_name: Name for the temporary repository directory |
| relative_build_dir: Path to the build dir in the current working dir to |
| clone build args from. |
| branch: Branch to checkout in the new repository. None will keep checked |
| out the same branch as the local repo. |
| Returns: |
| Path to the build directory of the new repository. |
| """ |
| cwd = os.getcwd() |
| |
| repo_dir = tempfile.mkdtemp(suffix='-%s' % dir_name) |
| src_dir = os.path.join(repo_dir, 'pdfium') |
| |
| self.git.CloneLocal(os.getcwd(), src_dir) |
| |
| if branch is not None: |
| os.chdir(src_dir) |
| self.git.Checkout(branch) |
| |
| os.chdir(repo_dir) |
| PrintErr('Syncing...') |
| |
| cmd = ['gclient', 'config', '--unmanaged', |
| 'https://pdfium.googlesource.com/pdfium.git'] |
| if self.args.cache_dir: |
| cmd.append('--cache-dir=%s' % self.args.cache_dir) |
| RunCommandPropagateErr(cmd, exit_status_on_error=1) |
| |
| RunCommandPropagateErr(['gclient', 'sync'], exit_status_on_error=1) |
| |
| PrintErr('Done.') |
| |
| build_dir = os.path.join(src_dir, relative_build_dir) |
| os.makedirs(build_dir) |
| os.chdir(src_dir) |
| |
| source_gn_args = os.path.join(cwd, relative_build_dir, 'args.gn') |
| dest_gn_args = os.path.join(build_dir, 'args.gn') |
| shutil.copy(source_gn_args, dest_gn_args) |
| |
| RunCommandPropagateErr(['gn', 'gen', relative_build_dir], |
| exit_status_on_error=1) |
| |
| os.chdir(cwd) |
| |
| return build_dir |
| |
| |
| def _CheckoutBranch(self, branch): |
| PrintErr("Checking out branch '%s'" % branch) |
| self.git.Checkout(branch) |
| |
| def _StashLocalChanges(self): |
| PrintErr('Stashing local changes') |
| return self.git.StashPush() |
| |
| def _RestoreLocalChanges(self): |
| PrintErr('Restoring local changes') |
| self.git.StashPopAll() |
| |
| def _BuildCurrentBranch(self, build_dir): |
| """Synchronizes and builds the current version of pdfium. |
| |
| Args: |
| build_dir: String with path to build directory |
| """ |
| PrintErr('Syncing...') |
| RunCommandPropagateErr(['gclient', 'sync'], exit_status_on_error=1) |
| PrintErr('Done.') |
| |
| PrintErr('Building...') |
| cmd = ['ninja', '-C', build_dir, 'pdfium_test'] |
| if GetBooleanGnArg('use_goma', build_dir): |
| cmd.extend(['-j', '250']) |
| RunCommandPropagateErr(cmd, stdout_has_errors=True, exit_status_on_error=1) |
| PrintErr('Done.') |
| |
| def _MeasureCurrentBranch(self, run_label, build_dir): |
| PrintErr('Measuring...') |
| if self.args.num_workers > 1 and len(self.test_cases) > 1: |
| results = self._RunAsync(run_label, build_dir) |
| else: |
| results = self._RunSync(run_label, build_dir) |
| PrintErr('Done.') |
| |
| return results |
| |
| def _RunSync(self, run_label, build_dir): |
| """Profiles the test cases synchronously. |
| |
| Args: |
| run_label: String to differentiate this version of the code in output |
| files from other versions. |
| build_dir: String with path to build directory |
| |
| Returns: |
| A dict mapping each test case name to the profiling values for that |
| test case. |
| """ |
| results = {} |
| |
| for test_case in self.test_cases: |
| result = self.RunSingleTestCase(run_label, build_dir, test_case) |
| if result is not None: |
| results[test_case] = result |
| |
| return results |
| |
| def _RunAsync(self, run_label, build_dir): |
| """Profiles the test cases asynchronously. |
| |
| Uses as many workers as configured by --num-workers. |
| |
| Args: |
| run_label: String to differentiate this version of the code in output |
| files from other versions. |
| build_dir: String with path to build directory |
| |
| Returns: |
| A dict mapping each test case name to the profiling values for that |
| test case. |
| """ |
| results = {} |
| pool = multiprocessing.Pool(self.args.num_workers) |
| worker_func = functools.partial( |
| RunSingleTestCaseParallel, self, run_label, build_dir) |
| |
| try: |
| # The timeout is a workaround for http://bugs.python.org/issue8296 |
| # which prevents KeyboardInterrupt from working. |
| one_year_in_seconds = 3600 * 24 * 365 |
| worker_results = (pool.map_async(worker_func, self.test_cases) |
| .get(one_year_in_seconds)) |
| for worker_result in worker_results: |
| test_case, result = worker_result |
| if result is not None: |
| results[test_case] = result |
| except KeyboardInterrupt: |
| pool.terminate() |
| sys.exit(1) |
| else: |
| pool.close() |
| |
| pool.join() |
| |
| return results |
| |
| def RunSingleTestCase(self, run_label, build_dir, test_case): |
| """Profiles a single test case. |
| |
| Args: |
| run_label: String to differentiate this version of the code in output |
| files from other versions. |
| build_dir: String with path to build directory |
| test_case: Path to the test case. |
| |
| Returns: |
| The measured profiling value for that test case. |
| """ |
| command = [self.safe_measure_script_path, test_case, |
| '--build-dir=%s' % build_dir] |
| |
| if self.args.interesting_section: |
| command.append('--interesting-section') |
| |
| if self.args.profiler: |
| command.append('--profiler=%s' % self.args.profiler) |
| |
| profile_file_path = self._GetProfileFilePath(run_label, test_case) |
| if profile_file_path: |
| command.append('--output-path=%s' % profile_file_path) |
| |
| if self.args.png_dir: |
| command.append('--png') |
| |
| if self.args.pages: |
| command.extend(['--pages', self.args.pages]) |
| |
| output = RunCommandPropagateErr(command) |
| |
| if output is None: |
| return None |
| |
| if self.args.png_dir: |
| self._MoveImages(test_case, run_label) |
| |
| # Get the time number as output, making sure it's just a number |
| output = output.strip() |
| if re.match('^[0-9]+$', output): |
| return int(output) |
| |
| return None |
| |
| def _MoveImages(self, test_case, run_label): |
| png_dir = os.path.join(self.args.png_dir, run_label) |
| if not os.path.exists(png_dir): |
| os.makedirs(png_dir) |
| |
| test_case_dir, test_case_filename = os.path.split(test_case) |
| test_case_png_matcher = '%s.*.png' % test_case_filename |
| for output_png in glob.glob(os.path.join(test_case_dir, |
| test_case_png_matcher)): |
| shutil.move(output_png, png_dir) |
| |
| def _GetProfileFilePath(self, run_label, test_case): |
| if self.args.output_dir: |
| output_filename = ('callgrind.out.%s.%s' |
| % (test_case.replace('/', '_'), |
| run_label)) |
| return os.path.join(self.args.output_dir, output_filename) |
| else: |
| return None |
| |
| def _DrawConclusions(self, times_before_branch, times_after_branch): |
| """Draws conclusions comparing results of test runs in two branches. |
| |
| Args: |
| times_before_branch: A dict mapping each test case name to the |
| profiling values for that test case in the branch to be considered |
| as the baseline. |
| times_after_branch: A dict mapping each test case name to the |
| profiling values for that test case in the branch to be considered |
| as the new version. |
| |
| Returns: |
| ComparisonConclusions with all test cases processed. |
| """ |
| conclusions = ComparisonConclusions(self.args.threshold_significant) |
| |
| for test_case in sorted(self.test_cases): |
| before = times_before_branch.get(test_case) |
| after = times_after_branch.get(test_case) |
| conclusions.ProcessCase(test_case, before, after) |
| |
| return conclusions |
| |
| def _PrintConclusions(self, conclusions_dict): |
| """Prints the conclusions as the script output. |
| |
| Depending on the script args, this can output a human or a machine-readable |
| version of the conclusions. |
| |
| Args: |
| conclusions_dict: Dict to print returned from |
| ComparisonConclusions.GetOutputDict(). |
| """ |
| if self.args.machine_readable: |
| print json.dumps(conclusions_dict) |
| else: |
| PrintConclusionsDictHumanReadable( |
| conclusions_dict, colored=True, key=self.args.case_order) |
| |
| def _CleanUp(self, conclusions): |
| """Removes profile output files for uninteresting cases. |
| |
| Cases without significant regressions or improvements and considered |
| uninteresting. |
| |
| Args: |
| conclusions: A ComparisonConclusions. |
| """ |
| if not self.args.output_dir: |
| return |
| |
| if self.args.profiler != 'callgrind': |
| return |
| |
| for case_result in conclusions.GetCaseResults().values(): |
| if case_result.rating not in [RATING_REGRESSION, RATING_IMPROVEMENT]: |
| self._CleanUpOutputFile('before', case_result.case_name) |
| self._CleanUpOutputFile('after', case_result.case_name) |
| |
| def _CleanUpOutputFile(self, run_label, case_name): |
| """Removes one profile output file. |
| |
| If the output file does not exist, fails silently. |
| |
| Args: |
| run_label: String to differentiate a version of the code in output |
| files from other versions. |
| case_name: String identifying test case for which to remove the output |
| file. |
| """ |
| try: |
| os.remove(self._GetProfileFilePath(run_label, case_name)) |
| except OSError: |
| pass |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('input_paths', nargs='+', |
| help='pdf files or directories to search for pdf files ' |
| 'to run as test cases') |
| parser.add_argument('--branch-before', |
| help='git branch to use as "before" for comparison. ' |
| 'Omitting this will use the current branch ' |
| 'without uncommitted changes as the baseline.') |
| parser.add_argument('--branch-after', |
| help='git branch to use as "after" for comparison. ' |
| 'Omitting this will use the current branch ' |
| 'with uncommitted changes.') |
| parser.add_argument('--build-dir', default=os.path.join('out', 'Release'), |
| help='relative path from the base source directory ' |
| 'to the build directory') |
| parser.add_argument('--build-dir-before', |
| help='relative path from the base source directory ' |
| 'to the build directory for the "before" branch, if ' |
| 'different from the build directory for the ' |
| '"after" branch') |
| parser.add_argument('--cache-dir', default=None, |
| help='directory with a new or preexisting cache for ' |
| 'downloads. Default is to not use a cache.') |
| parser.add_argument('--this-repo', action='store_true', |
| help='use the repository where the script is instead of ' |
| 'checking out a temporary one. This is faster and ' |
| 'does not require downloads, but although it ' |
| 'restores the state of the local repo, if the ' |
| 'script is killed or crashes the changes can remain ' |
| 'stashed and you may be on another branch.') |
| parser.add_argument('--profiler', default='callgrind', |
| help='which profiler to use. Supports callgrind, ' |
| 'perfstat, and none. Default is callgrind.') |
| parser.add_argument('--interesting-section', action='store_true', |
| help='whether to measure just the interesting section or ' |
| 'the whole test harness. Limiting to only the ' |
| 'interesting section does not work on Release since ' |
| 'the delimiters are optimized out') |
| parser.add_argument('--pages', |
| help='selects some pages to be rendered. Page numbers ' |
| 'are 0-based. "--pages A" will render only page A. ' |
| '"--pages A-B" will render pages A to B ' |
| '(inclusive).') |
| parser.add_argument('--num-workers', default=multiprocessing.cpu_count(), |
| type=int, help='run NUM_WORKERS jobs in parallel') |
| parser.add_argument('--output-dir', |
| help='directory to write the profile data output files') |
| parser.add_argument('--png-dir', default=None, |
| help='outputs pngs to the specified directory that can ' |
| 'be compared with a static html generated. Will ' |
| 'affect performance measurements.') |
| parser.add_argument('--png-threshold', default=0.0, type=float, |
| help='Requires --png-dir. Threshold above which a png ' |
| 'is considered to have changed.') |
| parser.add_argument('--threshold-significant', default=0.02, type=float, |
| help='variations in performance above this factor are ' |
| 'considered significant') |
| parser.add_argument('--machine-readable', action='store_true', |
| help='whether to get output for machines. If enabled the ' |
| 'output will be a json with the format specified in ' |
| 'ComparisonConclusions.GetOutputDict(). Default is ' |
| 'human-readable.') |
| parser.add_argument('--case-order', default=None, |
| help='what key to use when sorting test cases in the ' |
| 'output. Accepted values are "after", "before", ' |
| '"ratio" and "rating". Default is sorting by test ' |
| 'case path.') |
| |
| args = parser.parse_args() |
| |
| # Always start at the pdfium src dir, which is assumed to be two level above |
| # this script. |
| pdfium_src_dir = os.path.join( |
| os.path.dirname(__file__), |
| os.path.pardir, |
| os.path.pardir) |
| os.chdir(pdfium_src_dir) |
| |
| git = GitHelper() |
| |
| if args.branch_after and not args.branch_before: |
| PrintErr('--branch-after requires --branch-before to be specified.') |
| return 1 |
| |
| if args.branch_after and not git.BranchExists(args.branch_after): |
| PrintErr('Branch "%s" does not exist' % args.branch_after) |
| return 1 |
| |
| if args.branch_before and not git.BranchExists(args.branch_before): |
| PrintErr('Branch "%s" does not exist' % args.branch_before) |
| return 1 |
| |
| if args.output_dir: |
| args.output_dir = os.path.expanduser(args.output_dir) |
| if not os.path.isdir(args.output_dir): |
| PrintErr('"%s" is not a directory' % args.output_dir) |
| return 1 |
| |
| if args.png_dir: |
| args.png_dir = os.path.expanduser(args.png_dir) |
| if not os.path.isdir(args.png_dir): |
| PrintErr('"%s" is not a directory' % args.png_dir) |
| return 1 |
| |
| if args.threshold_significant <= 0.0: |
| PrintErr('--threshold-significant should receive a positive float') |
| return 1 |
| |
| if args.png_threshold: |
| if not args.png_dir: |
| PrintErr('--png-threshold requires --png-dir to be specified.') |
| return 1 |
| |
| if args.png_threshold <= 0.0: |
| PrintErr('--png-threshold should receive a positive float') |
| return 1 |
| |
| if args.pages: |
| if not re.match(r'^\d+(-\d+)?$', args.pages): |
| PrintErr('Supported formats for --pages are "--pages 7" and ' |
| '"--pages 3-6"') |
| return 1 |
| |
| run = CompareRun(args) |
| return run.Run() |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |