#!/usr/bin/env python3
# Copyright 2017 The PDFium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Looks for performance regressions on all pushes since the last run.

Run this nightly to have a periodical check for performance regressions.

Stores the results for each run and last checkpoint in a results directory.
"""

import argparse
import datetime
import json
import os
import sys

from common import PrintWithTime
from common import RunCommandPropagateErr
from githelper import GitHelper
from safetynet_conclusions import PrintConclusionsDictHumanReadable


class JobContext:
  """Context for a single run, including name and directory paths."""

  def __init__(self, args):
    self.datetime = datetime.datetime.now().strftime('%Y-%m-%d-%H-%M-%S')
    self.results_dir = args.results_dir
    self.last_revision_covered_file = os.path.join(self.results_dir,
                                                   'last_revision_covered')
    self.run_output_dir = os.path.join(self.results_dir,
                                       'profiles_%s' % self.datetime)
    self.run_output_log_file = os.path.join(self.results_dir,
                                            '%s.log' % self.datetime)


class JobRun:
  """A single run looking for regressions since the last one."""

  def __init__(self, args, context):
    """Constructor.

    Args:
      args: Namespace with arguments passed to the script.
      context: JobContext for this run.
    """
    self.git = GitHelper()
    self.args = args
    self.context = context

  def Run(self):
    """Searches for regressions.

    Will only write a checkpoint when first run, and on all subsequent runs
    a comparison is done against the last checkpoint.

    Returns:
      Exit code for the script: 0 if no significant changes are found; 1 if
      there was an error in the comparison; 3 if there was a regression; 4 if
      there was an improvement and no regression.
    """
    pdfium_src_dir = os.path.join(
        os.path.dirname(__file__), os.path.pardir, os.path.pardir)
    os.chdir(pdfium_src_dir)

    branch_to_restore = self.git.GetCurrentBranchName()

    if not self.args.no_checkout:
      self.git.FetchOriginMaster()
      self.git.Checkout('origin/main')

    # Make sure results dir exists
    if not os.path.exists(self.context.results_dir):
      os.makedirs(self.context.results_dir)

    if not os.path.exists(self.context.last_revision_covered_file):
      result = self._InitialRun()
    else:
      with open(self.context.last_revision_covered_file) as f:
        last_revision_covered = f.read().strip()
      result = self._IncrementalRun(last_revision_covered)

    self.git.Checkout(branch_to_restore)
    return result

  def _InitialRun(self):
    """Initial run, just write a checkpoint.

    Returns:
      Exit code for the script.
    """
    current = self.git.GetCurrentBranchHash()

    PrintWithTime('Initial run, current is %s' % current)

    self._WriteCheckpoint(current)

    PrintWithTime('All set up, next runs will be incremental and perform '
                  'comparisons')
    return 0

  def _IncrementalRun(self, last_revision_covered):
    """Incremental run, compare against last checkpoint and update it.

    Args:
      last_revision_covered: String with hash for last checkpoint.

    Returns:
      Exit code for the script.
    """
    current = self.git.GetCurrentBranchHash()

    PrintWithTime('Incremental run, current is %s, last is %s' %
                  (current, last_revision_covered))

    if not os.path.exists(self.context.run_output_dir):
      os.makedirs(self.context.run_output_dir)

    if current == last_revision_covered:
      PrintWithTime('No changes seen, finishing job')
      output_info = {
          'metadata':
              self._BuildRunMetadata(last_revision_covered, current, False)
      }
      self._WriteRawJson(output_info)
      return 0

    # Run compare
    cmd = [
        'testing/tools/safetynet_compare.py', '--this-repo',
        '--machine-readable',
        '--branch-before=%s' % last_revision_covered,
        '--output-dir=%s' % self.context.run_output_dir
    ]
    cmd.extend(self.args.input_paths)

    json_output = RunCommandPropagateErr(cmd)

    if json_output is None:
      return 1

    output_info = json.loads(json_output)

    run_metadata = self._BuildRunMetadata(last_revision_covered, current, True)
    output_info.setdefault('metadata', {}).update(run_metadata)
    self._WriteRawJson(output_info)

    PrintConclusionsDictHumanReadable(
        output_info,
        colored=(not self.args.output_to_log and not self.args.no_color),
        key='after')

    status = 0

    if output_info['summary']['improvement']:
      PrintWithTime('Improvement detected.')
      status = 4

    if output_info['summary']['regression']:
      PrintWithTime('Regression detected.')
      status = 3

    if status == 0:
      PrintWithTime('Nothing detected.')

    self._WriteCheckpoint(current)

    return status

  def _WriteRawJson(self, output_info):
    json_output_file = os.path.join(self.context.run_output_dir, 'raw.json')
    with open(json_output_file, 'w') as f:
      json.dump(output_info, f)

  def _BuildRunMetadata(self, revision_before, revision_after,
                        comparison_performed):
    return {
        'datetime': self.context.datetime,
        'revision_before': revision_before,
        'revision_after': revision_after,
        'comparison_performed': comparison_performed,
    }

  def _WriteCheckpoint(self, checkpoint):
    if not self.args.no_checkpoint:
      with open(self.context.last_revision_covered_file, 'w') as f:
        f.write(checkpoint + '\n')


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('results_dir', help='where to write the job results')
  parser.add_argument(
      'input_paths',
      nargs='+',
      help='pdf files or directories to search for pdf files '
      'to run as test cases')
  parser.add_argument(
      '--no-checkout',
      action='store_true',
      help='whether to skip checking out origin/main. Use '
      'for script debugging.')
  parser.add_argument(
      '--no-checkpoint',
      action='store_true',
      help='whether to skip writing the new checkpoint. Use '
      'for script debugging.')
  parser.add_argument(
      '--no-color',
      action='store_true',
      help='whether to write output without color escape '
      'codes.')
  parser.add_argument(
      '--output-to-log',
      action='store_true',
      help='whether to write output to a log file')
  args = parser.parse_args()

  job_context = JobContext(args)

  if args.output_to_log:
    log_file = open(job_context.run_output_log_file, 'w')
    sys.stdout = log_file
    sys.stderr = log_file

  run = JobRun(args, job_context)
  result = run.Run()

  if args.output_to_log:
    log_file.close()

  return result


if __name__ == '__main__':
  sys.exit(main())
