xref: /aosp_15_r20/external/pdfium/testing/tools/safetynet_job.py (revision 3ac0a46f773bac49fa9476ec2b1cf3f8da5ec3a4)
1*3ac0a46fSAndroid Build Coastguard Worker#!/usr/bin/env python3
2*3ac0a46fSAndroid Build Coastguard Worker# Copyright 2017 The PDFium Authors
3*3ac0a46fSAndroid Build Coastguard Worker# Use of this source code is governed by a BSD-style license that can be
4*3ac0a46fSAndroid Build Coastguard Worker# found in the LICENSE file.
5*3ac0a46fSAndroid Build Coastguard Worker"""Looks for performance regressions on all pushes since the last run.
6*3ac0a46fSAndroid Build Coastguard Worker
7*3ac0a46fSAndroid Build Coastguard WorkerRun this nightly to have a periodical check for performance regressions.
8*3ac0a46fSAndroid Build Coastguard Worker
9*3ac0a46fSAndroid Build Coastguard WorkerStores the results for each run and last checkpoint in a results directory.
10*3ac0a46fSAndroid Build Coastguard Worker"""
11*3ac0a46fSAndroid Build Coastguard Worker
12*3ac0a46fSAndroid Build Coastguard Workerimport argparse
13*3ac0a46fSAndroid Build Coastguard Workerimport datetime
14*3ac0a46fSAndroid Build Coastguard Workerimport json
15*3ac0a46fSAndroid Build Coastguard Workerimport os
16*3ac0a46fSAndroid Build Coastguard Workerimport sys
17*3ac0a46fSAndroid Build Coastguard Worker
18*3ac0a46fSAndroid Build Coastguard Workerfrom common import PrintWithTime
19*3ac0a46fSAndroid Build Coastguard Workerfrom common import RunCommandPropagateErr
20*3ac0a46fSAndroid Build Coastguard Workerfrom githelper import GitHelper
21*3ac0a46fSAndroid Build Coastguard Workerfrom safetynet_conclusions import PrintConclusionsDictHumanReadable
22*3ac0a46fSAndroid Build Coastguard Worker
23*3ac0a46fSAndroid Build Coastguard Worker
24*3ac0a46fSAndroid Build Coastguard Workerclass JobContext:
25*3ac0a46fSAndroid Build Coastguard Worker  """Context for a single run, including name and directory paths."""
26*3ac0a46fSAndroid Build Coastguard Worker
27*3ac0a46fSAndroid Build Coastguard Worker  def __init__(self, args):
28*3ac0a46fSAndroid Build Coastguard Worker    self.datetime = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
29*3ac0a46fSAndroid Build Coastguard Worker    self.results_dir = args.results_dir
30*3ac0a46fSAndroid Build Coastguard Worker    self.last_revision_covered_file = os.path.join(self.results_dir,
31*3ac0a46fSAndroid Build Coastguard Worker                                                   'last_revision_covered')
32*3ac0a46fSAndroid Build Coastguard Worker    self.run_output_dir = os.path.join(self.results_dir,
33*3ac0a46fSAndroid Build Coastguard Worker                                       'profiles_%s' % self.datetime)
34*3ac0a46fSAndroid Build Coastguard Worker    self.run_output_log_file = os.path.join(self.results_dir,
35*3ac0a46fSAndroid Build Coastguard Worker                                            '%s.log' % self.datetime)
36*3ac0a46fSAndroid Build Coastguard Worker
37*3ac0a46fSAndroid Build Coastguard Worker
38*3ac0a46fSAndroid Build Coastguard Workerclass JobRun:
39*3ac0a46fSAndroid Build Coastguard Worker  """A single run looking for regressions since the last one."""
40*3ac0a46fSAndroid Build Coastguard Worker
41*3ac0a46fSAndroid Build Coastguard Worker  def __init__(self, args, context):
42*3ac0a46fSAndroid Build Coastguard Worker    """Constructor.
43*3ac0a46fSAndroid Build Coastguard Worker
44*3ac0a46fSAndroid Build Coastguard Worker    Args:
45*3ac0a46fSAndroid Build Coastguard Worker      args: Namespace with arguments passed to the script.
46*3ac0a46fSAndroid Build Coastguard Worker      context: JobContext for this run.
47*3ac0a46fSAndroid Build Coastguard Worker    """
48*3ac0a46fSAndroid Build Coastguard Worker    self.git = GitHelper()
49*3ac0a46fSAndroid Build Coastguard Worker    self.args = args
50*3ac0a46fSAndroid Build Coastguard Worker    self.context = context
51*3ac0a46fSAndroid Build Coastguard Worker
52*3ac0a46fSAndroid Build Coastguard Worker  def Run(self):
53*3ac0a46fSAndroid Build Coastguard Worker    """Searches for regressions.
54*3ac0a46fSAndroid Build Coastguard Worker
55*3ac0a46fSAndroid Build Coastguard Worker    Will only write a checkpoint when first run, and on all subsequent runs
56*3ac0a46fSAndroid Build Coastguard Worker    a comparison is done against the last checkpoint.
57*3ac0a46fSAndroid Build Coastguard Worker
58*3ac0a46fSAndroid Build Coastguard Worker    Returns:
59*3ac0a46fSAndroid Build Coastguard Worker      Exit code for the script: 0 if no significant changes are found; 1 if
60*3ac0a46fSAndroid Build Coastguard Worker      there was an error in the comparison; 3 if there was a regression; 4 if
61*3ac0a46fSAndroid Build Coastguard Worker      there was an improvement and no regression.
62*3ac0a46fSAndroid Build Coastguard Worker    """
63*3ac0a46fSAndroid Build Coastguard Worker    pdfium_src_dir = os.path.join(
64*3ac0a46fSAndroid Build Coastguard Worker        os.path.dirname(__file__), os.path.pardir, os.path.pardir)
65*3ac0a46fSAndroid Build Coastguard Worker    os.chdir(pdfium_src_dir)
66*3ac0a46fSAndroid Build Coastguard Worker
67*3ac0a46fSAndroid Build Coastguard Worker    branch_to_restore = self.git.GetCurrentBranchName()
68*3ac0a46fSAndroid Build Coastguard Worker
69*3ac0a46fSAndroid Build Coastguard Worker    if not self.args.no_checkout:
70*3ac0a46fSAndroid Build Coastguard Worker      self.git.FetchOriginMaster()
71*3ac0a46fSAndroid Build Coastguard Worker      self.git.Checkout('origin/main')
72*3ac0a46fSAndroid Build Coastguard Worker
73*3ac0a46fSAndroid Build Coastguard Worker    # Make sure results dir exists
74*3ac0a46fSAndroid Build Coastguard Worker    if not os.path.exists(self.context.results_dir):
75*3ac0a46fSAndroid Build Coastguard Worker      os.makedirs(self.context.results_dir)
76*3ac0a46fSAndroid Build Coastguard Worker
77*3ac0a46fSAndroid Build Coastguard Worker    if not os.path.exists(self.context.last_revision_covered_file):
78*3ac0a46fSAndroid Build Coastguard Worker      result = self._InitialRun()
79*3ac0a46fSAndroid Build Coastguard Worker    else:
80*3ac0a46fSAndroid Build Coastguard Worker      with open(self.context.last_revision_covered_file) as f:
81*3ac0a46fSAndroid Build Coastguard Worker        last_revision_covered = f.read().strip()
82*3ac0a46fSAndroid Build Coastguard Worker      result = self._IncrementalRun(last_revision_covered)
83*3ac0a46fSAndroid Build Coastguard Worker
84*3ac0a46fSAndroid Build Coastguard Worker    self.git.Checkout(branch_to_restore)
85*3ac0a46fSAndroid Build Coastguard Worker    return result
86*3ac0a46fSAndroid Build Coastguard Worker
87*3ac0a46fSAndroid Build Coastguard Worker  def _InitialRun(self):
88*3ac0a46fSAndroid Build Coastguard Worker    """Initial run, just write a checkpoint.
89*3ac0a46fSAndroid Build Coastguard Worker
90*3ac0a46fSAndroid Build Coastguard Worker    Returns:
91*3ac0a46fSAndroid Build Coastguard Worker      Exit code for the script.
92*3ac0a46fSAndroid Build Coastguard Worker    """
93*3ac0a46fSAndroid Build Coastguard Worker    current = self.git.GetCurrentBranchHash()
94*3ac0a46fSAndroid Build Coastguard Worker
95*3ac0a46fSAndroid Build Coastguard Worker    PrintWithTime('Initial run, current is %s' % current)
96*3ac0a46fSAndroid Build Coastguard Worker
97*3ac0a46fSAndroid Build Coastguard Worker    self._WriteCheckpoint(current)
98*3ac0a46fSAndroid Build Coastguard Worker
99*3ac0a46fSAndroid Build Coastguard Worker    PrintWithTime('All set up, next runs will be incremental and perform '
100*3ac0a46fSAndroid Build Coastguard Worker                  'comparisons')
101*3ac0a46fSAndroid Build Coastguard Worker    return 0
102*3ac0a46fSAndroid Build Coastguard Worker
103*3ac0a46fSAndroid Build Coastguard Worker  def _IncrementalRun(self, last_revision_covered):
104*3ac0a46fSAndroid Build Coastguard Worker    """Incremental run, compare against last checkpoint and update it.
105*3ac0a46fSAndroid Build Coastguard Worker
106*3ac0a46fSAndroid Build Coastguard Worker    Args:
107*3ac0a46fSAndroid Build Coastguard Worker      last_revision_covered: String with hash for last checkpoint.
108*3ac0a46fSAndroid Build Coastguard Worker
109*3ac0a46fSAndroid Build Coastguard Worker    Returns:
110*3ac0a46fSAndroid Build Coastguard Worker      Exit code for the script.
111*3ac0a46fSAndroid Build Coastguard Worker    """
112*3ac0a46fSAndroid Build Coastguard Worker    current = self.git.GetCurrentBranchHash()
113*3ac0a46fSAndroid Build Coastguard Worker
114*3ac0a46fSAndroid Build Coastguard Worker    PrintWithTime('Incremental run, current is %s, last is %s' %
115*3ac0a46fSAndroid Build Coastguard Worker                  (current, last_revision_covered))
116*3ac0a46fSAndroid Build Coastguard Worker
117*3ac0a46fSAndroid Build Coastguard Worker    if not os.path.exists(self.context.run_output_dir):
118*3ac0a46fSAndroid Build Coastguard Worker      os.makedirs(self.context.run_output_dir)
119*3ac0a46fSAndroid Build Coastguard Worker
120*3ac0a46fSAndroid Build Coastguard Worker    if current == last_revision_covered:
121*3ac0a46fSAndroid Build Coastguard Worker      PrintWithTime('No changes seen, finishing job')
122*3ac0a46fSAndroid Build Coastguard Worker      output_info = {
123*3ac0a46fSAndroid Build Coastguard Worker          'metadata':
124*3ac0a46fSAndroid Build Coastguard Worker              self._BuildRunMetadata(last_revision_covered, current, False)
125*3ac0a46fSAndroid Build Coastguard Worker      }
126*3ac0a46fSAndroid Build Coastguard Worker      self._WriteRawJson(output_info)
127*3ac0a46fSAndroid Build Coastguard Worker      return 0
128*3ac0a46fSAndroid Build Coastguard Worker
129*3ac0a46fSAndroid Build Coastguard Worker    # Run compare
130*3ac0a46fSAndroid Build Coastguard Worker    cmd = [
131*3ac0a46fSAndroid Build Coastguard Worker        'testing/tools/safetynet_compare.py', '--this-repo',
132*3ac0a46fSAndroid Build Coastguard Worker        '--machine-readable',
133*3ac0a46fSAndroid Build Coastguard Worker        '--branch-before=%s' % last_revision_covered,
134*3ac0a46fSAndroid Build Coastguard Worker        '--output-dir=%s' % self.context.run_output_dir
135*3ac0a46fSAndroid Build Coastguard Worker    ]
136*3ac0a46fSAndroid Build Coastguard Worker    cmd.extend(self.args.input_paths)
137*3ac0a46fSAndroid Build Coastguard Worker
138*3ac0a46fSAndroid Build Coastguard Worker    json_output = RunCommandPropagateErr(cmd)
139*3ac0a46fSAndroid Build Coastguard Worker
140*3ac0a46fSAndroid Build Coastguard Worker    if json_output is None:
141*3ac0a46fSAndroid Build Coastguard Worker      return 1
142*3ac0a46fSAndroid Build Coastguard Worker
143*3ac0a46fSAndroid Build Coastguard Worker    output_info = json.loads(json_output)
144*3ac0a46fSAndroid Build Coastguard Worker
145*3ac0a46fSAndroid Build Coastguard Worker    run_metadata = self._BuildRunMetadata(last_revision_covered, current, True)
146*3ac0a46fSAndroid Build Coastguard Worker    output_info.setdefault('metadata', {}).update(run_metadata)
147*3ac0a46fSAndroid Build Coastguard Worker    self._WriteRawJson(output_info)
148*3ac0a46fSAndroid Build Coastguard Worker
149*3ac0a46fSAndroid Build Coastguard Worker    PrintConclusionsDictHumanReadable(
150*3ac0a46fSAndroid Build Coastguard Worker        output_info,
151*3ac0a46fSAndroid Build Coastguard Worker        colored=(not self.args.output_to_log and not self.args.no_color),
152*3ac0a46fSAndroid Build Coastguard Worker        key='after')
153*3ac0a46fSAndroid Build Coastguard Worker
154*3ac0a46fSAndroid Build Coastguard Worker    status = 0
155*3ac0a46fSAndroid Build Coastguard Worker
156*3ac0a46fSAndroid Build Coastguard Worker    if output_info['summary']['improvement']:
157*3ac0a46fSAndroid Build Coastguard Worker      PrintWithTime('Improvement detected.')
158*3ac0a46fSAndroid Build Coastguard Worker      status = 4
159*3ac0a46fSAndroid Build Coastguard Worker
160*3ac0a46fSAndroid Build Coastguard Worker    if output_info['summary']['regression']:
161*3ac0a46fSAndroid Build Coastguard Worker      PrintWithTime('Regression detected.')
162*3ac0a46fSAndroid Build Coastguard Worker      status = 3
163*3ac0a46fSAndroid Build Coastguard Worker
164*3ac0a46fSAndroid Build Coastguard Worker    if status == 0:
165*3ac0a46fSAndroid Build Coastguard Worker      PrintWithTime('Nothing detected.')
166*3ac0a46fSAndroid Build Coastguard Worker
167*3ac0a46fSAndroid Build Coastguard Worker    self._WriteCheckpoint(current)
168*3ac0a46fSAndroid Build Coastguard Worker
169*3ac0a46fSAndroid Build Coastguard Worker    return status
170*3ac0a46fSAndroid Build Coastguard Worker
171*3ac0a46fSAndroid Build Coastguard Worker  def _WriteRawJson(self, output_info):
172*3ac0a46fSAndroid Build Coastguard Worker    json_output_file = os.path.join(self.context.run_output_dir, 'raw.json')
173*3ac0a46fSAndroid Build Coastguard Worker    with open(json_output_file, 'w') as f:
174*3ac0a46fSAndroid Build Coastguard Worker      json.dump(output_info, f)
175*3ac0a46fSAndroid Build Coastguard Worker
176*3ac0a46fSAndroid Build Coastguard Worker  def _BuildRunMetadata(self, revision_before, revision_after,
177*3ac0a46fSAndroid Build Coastguard Worker                        comparison_performed):
178*3ac0a46fSAndroid Build Coastguard Worker    return {
179*3ac0a46fSAndroid Build Coastguard Worker        'datetime': self.context.datetime,
180*3ac0a46fSAndroid Build Coastguard Worker        'revision_before': revision_before,
181*3ac0a46fSAndroid Build Coastguard Worker        'revision_after': revision_after,
182*3ac0a46fSAndroid Build Coastguard Worker        'comparison_performed': comparison_performed,
183*3ac0a46fSAndroid Build Coastguard Worker    }
184*3ac0a46fSAndroid Build Coastguard Worker
185*3ac0a46fSAndroid Build Coastguard Worker  def _WriteCheckpoint(self, checkpoint):
186*3ac0a46fSAndroid Build Coastguard Worker    if not self.args.no_checkpoint:
187*3ac0a46fSAndroid Build Coastguard Worker      with open(self.context.last_revision_covered_file, 'w') as f:
188*3ac0a46fSAndroid Build Coastguard Worker        f.write(checkpoint + '\n')
189*3ac0a46fSAndroid Build Coastguard Worker
190*3ac0a46fSAndroid Build Coastguard Worker
191*3ac0a46fSAndroid Build Coastguard Workerdef main():
192*3ac0a46fSAndroid Build Coastguard Worker  parser = argparse.ArgumentParser()
193*3ac0a46fSAndroid Build Coastguard Worker  parser.add_argument('results_dir', help='where to write the job results')
194*3ac0a46fSAndroid Build Coastguard Worker  parser.add_argument(
195*3ac0a46fSAndroid Build Coastguard Worker      'input_paths',
196*3ac0a46fSAndroid Build Coastguard Worker      nargs='+',
197*3ac0a46fSAndroid Build Coastguard Worker      help='pdf files or directories to search for pdf files '
198*3ac0a46fSAndroid Build Coastguard Worker      'to run as test cases')
199*3ac0a46fSAndroid Build Coastguard Worker  parser.add_argument(
200*3ac0a46fSAndroid Build Coastguard Worker      '--no-checkout',
201*3ac0a46fSAndroid Build Coastguard Worker      action='store_true',
202*3ac0a46fSAndroid Build Coastguard Worker      help='whether to skip checking out origin/main. Use '
203*3ac0a46fSAndroid Build Coastguard Worker      'for script debugging.')
204*3ac0a46fSAndroid Build Coastguard Worker  parser.add_argument(
205*3ac0a46fSAndroid Build Coastguard Worker      '--no-checkpoint',
206*3ac0a46fSAndroid Build Coastguard Worker      action='store_true',
207*3ac0a46fSAndroid Build Coastguard Worker      help='whether to skip writing the new checkpoint. Use '
208*3ac0a46fSAndroid Build Coastguard Worker      'for script debugging.')
209*3ac0a46fSAndroid Build Coastguard Worker  parser.add_argument(
210*3ac0a46fSAndroid Build Coastguard Worker      '--no-color',
211*3ac0a46fSAndroid Build Coastguard Worker      action='store_true',
212*3ac0a46fSAndroid Build Coastguard Worker      help='whether to write output without color escape '
213*3ac0a46fSAndroid Build Coastguard Worker      'codes.')
214*3ac0a46fSAndroid Build Coastguard Worker  parser.add_argument(
215*3ac0a46fSAndroid Build Coastguard Worker      '--output-to-log',
216*3ac0a46fSAndroid Build Coastguard Worker      action='store_true',
217*3ac0a46fSAndroid Build Coastguard Worker      help='whether to write output to a log file')
218*3ac0a46fSAndroid Build Coastguard Worker  args = parser.parse_args()
219*3ac0a46fSAndroid Build Coastguard Worker
220*3ac0a46fSAndroid Build Coastguard Worker  job_context = JobContext(args)
221*3ac0a46fSAndroid Build Coastguard Worker
222*3ac0a46fSAndroid Build Coastguard Worker  if args.output_to_log:
223*3ac0a46fSAndroid Build Coastguard Worker    log_file = open(job_context.run_output_log_file, 'w')
224*3ac0a46fSAndroid Build Coastguard Worker    sys.stdout = log_file
225*3ac0a46fSAndroid Build Coastguard Worker    sys.stderr = log_file
226*3ac0a46fSAndroid Build Coastguard Worker
227*3ac0a46fSAndroid Build Coastguard Worker  run = JobRun(args, job_context)
228*3ac0a46fSAndroid Build Coastguard Worker  result = run.Run()
229*3ac0a46fSAndroid Build Coastguard Worker
230*3ac0a46fSAndroid Build Coastguard Worker  if args.output_to_log:
231*3ac0a46fSAndroid Build Coastguard Worker    log_file.close()
232*3ac0a46fSAndroid Build Coastguard Worker
233*3ac0a46fSAndroid Build Coastguard Worker  return result
234*3ac0a46fSAndroid Build Coastguard Worker
235*3ac0a46fSAndroid Build Coastguard Worker
236*3ac0a46fSAndroid Build Coastguard Workerif __name__ == '__main__':
237*3ac0a46fSAndroid Build Coastguard Worker  sys.exit(main())
238