#!/usr/bin/env python3
#
# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#

"""A tool for running puffin tests in a corpus of deflate compressed files."""

import argparse
import filecmp
import logging
import os
import subprocess
import sys
import tempfile

_PUFFHUFF = 'puffhuff'
_PUFFDIFF = 'puffdiff'
TESTS = (_PUFFHUFF, _PUFFDIFF)


class Error(Exception):
  """Puffin general processing error."""


def ParseArguments(argv):
  """Parses and Validates command line arguments.

  Args:
    argv: command line arguments to parse.

  Returns:
    The arguments list.
  """
  parser = argparse.ArgumentParser()

  parser.add_argument('corpus', metavar='CORPUS',
                      help='A corpus directory containing compressed files')
  parser.add_argument('-d', '--disabled_tests', default=(), metavar='',
                      nargs='*',
                      help=('Space separated list of tests to disable. '
                            'Allowed options include: ' + ', '.join(TESTS)),
                      choices=TESTS)
  parser.add_argument('--cache_size', type=int, metavar='SIZE',
                      help='The size (in bytes) of the cache for puffpatch '
                      'operations.')
  parser.add_argument('--debug', action='store_true',
                      help='Turns on verbosity.')

  # Parse command-line arguments.
  args = parser.parse_args(argv)

  if not os.path.isdir(args.corpus):
    raise Error('Corpus directory {} is non-existent or inaccesible'
                .format(args.corpus))
  return args


def main(argv):
  """The main function."""
  args = ParseArguments(argv[1:])

  if args.debug:
    logging.getLogger().setLevel(logging.DEBUG)

  # Construct list of appropriate files.
  files = list(filter(os.path.isfile, [os.path.join(args.corpus, f)
                                       for f in os.listdir(args.corpus)]))

  # For each file in corpus run puffhuff.
  if _PUFFHUFF not in args.disabled_tests:
    for src in files:
      with tempfile.NamedTemporaryFile() as tgt_file:

        operation = 'puffhuff'
        logging.debug('Running %s on %s', operation, src)
        cmd = ['puffin',
               '--operation={}'.format(operation),
               '--src_file={}'.format(src),
               '--dst_file={}'.format(tgt_file.name)]
        if subprocess.call(cmd) != 0:
          raise Error('Puffin failed to do {} command: {}'
                      .format(operation, cmd))

        if not filecmp.cmp(src, tgt_file.name):
          raise Error('The generated file {} is not equivalent to the original '
                      'file {} after {} operation.'
                      .format(tgt_file.name, src, operation))

  if _PUFFDIFF not in args.disabled_tests:
    # Run puffdiff and puffpatch for each pairs of files in the corpus.
    for src in files:
      for tgt in files:
        with tempfile.NamedTemporaryFile() as patch, \
             tempfile.NamedTemporaryFile() as new_tgt:

          operation = 'puffdiff'
          logging.debug('Running %s on %s (%d) and %s (%d)',
                        operation,
                        os.path.basename(src), os.stat(src).st_size,
                        os.path.basename(tgt), os.stat(tgt).st_size)
          cmd = ['puffin',
                 '--operation={}'.format(operation),
                 '--src_file={}'.format(src),
                 '--dst_file={}'.format(tgt),
                 '--patch_file={}'.format(patch.name)]

          # Running the puffdiff operation
          if subprocess.call(cmd) != 0:
            raise Error('Puffin failed to do {} command: {}'
                        .format(operation, cmd))

          logging.debug('Patch size is: %d', os.stat(patch.name).st_size)

          operation = 'puffpatch'
          logging.debug('Running %s on src file %s and patch %s',
                        operation, os.path.basename(src), patch.name)
          cmd = ['puffin',
                 '--operation={}'.format(operation),
                 '--src_file={}'.format(src),
                 '--dst_file={}'.format(new_tgt.name),
                 '--patch_file={}'.format(patch.name)]
          if args.cache_size:
            cmd += ['--cache_size={}'.format(args.cache_size)]

          # Running the puffpatch operation
          if subprocess.call(cmd) != 0:
            raise Error('Puffin failed to do {} command: {}'
                        .format(operation, cmd))

          if not filecmp.cmp(tgt, new_tgt.name):
            raise Error('The generated file {} is not equivalent to the '
                        'original file {} after puffpatch operation.'
                        .format(new_tgt.name, tgt))

  return 0


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