#!/usr/bin/env python2.7
# Copyright 2015, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Run tests in parallel."""

import argparse
import ast
import glob
import hashlib
import itertools
import json
import multiprocessing
import os
import platform
import random
import re
import socket
import subprocess
import sys
import tempfile
import traceback
import time
import urllib2
import uuid

import jobset
import report_utils
import watch_dirs


_ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), '../..'))
os.chdir(_ROOT)


_FORCE_ENVIRON_FOR_WRAPPERS = {}


def platform_string():
  return jobset.platform_string()


# SimpleConfig: just compile with CONFIG=config, and run the binary to test
class Config(object):

  def __init__(self, config, environ=None, timeout_multiplier=1, tool_prefix=[]):
    if environ is None:
      environ = {}
    self.build_config = config
    self.allow_hashing = (config != 'gcov')
    self.environ = environ
    self.environ['CONFIG'] = config
    self.tool_prefix = tool_prefix
    self.timeout_multiplier = timeout_multiplier

  def job_spec(self, cmdline, hash_targets, timeout_seconds=5*60,
               shortname=None, environ={}, cpu_cost=1.0, flaky=False):
    """Construct a jobset.JobSpec for a test under this config

       Args:
         cmdline:      a list of strings specifying the command line the test
                       would like to run
         hash_targets: either None (don't do caching of test results), or
                       a list of strings specifying files to include in a
                       binary hash to check if a test has changed
                       -- if used, all artifacts needed to run the test must
                          be listed
    """
    actual_environ = self.environ.copy()
    for k, v in environ.iteritems():
      actual_environ[k] = v
    return jobset.JobSpec(cmdline=self.tool_prefix + cmdline,
                          shortname=shortname,
                          environ=actual_environ,
                          cpu_cost=cpu_cost,
                          timeout_seconds=(self.timeout_multiplier * timeout_seconds if timeout_seconds else None),
                          hash_targets=hash_targets
                              if self.allow_hashing else None,
                          flake_retries=5 if flaky or args.allow_flakes else 0,
                          timeout_retries=3 if args.allow_flakes else 0)


def get_c_tests(travis, test_lang) :
  out = []
  platforms_str = 'ci_platforms' if travis else 'platforms'
  with open('tools/run_tests/tests.json') as f:
    js = json.load(f)
    return [tgt
            for tgt in js
            if tgt['language'] == test_lang and
                platform_string() in tgt[platforms_str] and
                not (travis and tgt['flaky'])]


def _check_compiler(compiler, supported_compilers):
  if compiler not in supported_compilers:
    raise Exception('Compiler %s not supported (on this platform).' % compiler)


def _check_arch(arch, supported_archs):
  if arch not in supported_archs:
    raise Exception('Architecture %s not supported.' % arch)


def _is_use_docker_child():
  """Returns True if running running as a --use_docker child."""
  return True if os.getenv('RUN_TESTS_COMMAND') else False


class CLanguage(object):

  def __init__(self, make_target, test_lang):
    self.make_target = make_target
    self.platform = platform_string()
    self.test_lang = test_lang

  def configure(self, config, args):
    self.config = config
    self.args = args
    if self.platform == 'windows':
      self._make_options = [_windows_toolset_option(self.args.compiler),
                            _windows_arch_option(self.args.arch)]
    else:
      self._docker_distro, self._make_options = self._compiler_options(self.args.use_docker,
                                                                       self.args.compiler)

  def test_specs(self):
    out = []
    binaries = get_c_tests(self.args.travis, self.test_lang)
    for target in binaries:
      if self.config.build_config in target['exclude_configs']:
        continue
      if self.platform == 'windows':
        binary = 'vsprojects/%s%s/%s.exe' % (
            'x64/' if self.args.arch == 'x64' else '',
            _MSBUILD_CONFIG[self.config.build_config],
            target['name'])
      else:
        binary = 'bins/%s/%s' % (self.config.build_config, target['name'])
      if os.path.isfile(binary):
        if 'gtest' in target and target['gtest']:
          # here we parse the output of --gtest_list_tests to build up a
          # complete list of the tests contained in a binary
          # for each test, we then add a job to run, filtering for just that
          # test
          with open(os.devnull, 'w') as fnull:
            tests = subprocess.check_output([binary, '--gtest_list_tests'],
                                            stderr=fnull)
          base = None
          for line in tests.split('\n'):
            i = line.find('#')
            if i >= 0: line = line[:i]
            if not line: continue
            if line[0] != ' ':
              base = line.strip()
            else:
              assert base is not None
              assert line[1] == ' '
              test = base + line.strip()
              cmdline = [binary] + ['--gtest_filter=%s' % test]
              out.append(self.config.job_spec(cmdline, [binary],
                                              shortname='%s:%s' % (binary, test),
                                              cpu_cost=target['cpu_cost'],
                                              environ={'GRPC_DEFAULT_SSL_ROOTS_FILE_PATH':
                                                       _ROOT + '/src/core/lib/tsi/test_creds/ca.pem'}))
        else:
          cmdline = [binary] + target['args']
          out.append(self.config.job_spec(cmdline, [binary],
                                          shortname=' '.join(cmdline),
                                          cpu_cost=target['cpu_cost'],
                                          flaky=target.get('flaky', False),
                                          environ={'GRPC_DEFAULT_SSL_ROOTS_FILE_PATH':
                                                   _ROOT + '/src/core/lib/tsi/test_creds/ca.pem'}))
      elif self.args.regex == '.*' or self.platform == 'windows':
        print '\nWARNING: binary not found, skipping', binary
    return sorted(out)

  def make_targets(self):
    test_regex = self.args.regex
    if self.platform != 'windows' and self.args.regex != '.*':
      # use the regex to minimize the number of things to build
      return [os.path.basename(target['name'])
              for target in get_c_tests(False, self.test_lang)
              if re.search(test_regex, '/' + target['name'])]
    if self.platform == 'windows':
      # don't build tools on windows just yet
      return ['buildtests_%s' % self.make_target]
    return ['buildtests_%s' % self.make_target, 'tools_%s' % self.make_target]

  def make_options(self):
    return self._make_options;

  def pre_build_steps(self):
    if self.platform == 'windows':
      return [['tools\\run_tests\\pre_build_c.bat']]