# pylint: disable=g-bad-file-header
# Copyright 2019 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import print_function

import os
import shutil
import subprocess
import textwrap
import unittest

from src.test.py.bazel import test_base


class MockPythonLines(object):

  NORMAL = textwrap.dedent(
      r"""\
      if [ "$1" = "-V" ]; then
          echo "Mock Python 3.xyz!"
      else
          echo "I am mock Python!"
      fi
      """
  ).split("\n")

  FAIL = textwrap.dedent(r"""\
      echo "Mock failure!"
      exit 1
      """).split("\n")

  VERSION_ERROR = textwrap.dedent(r"""\
      if [ "$1" = "-V" ]; then
          echo "Error!"
          exit 1
      else
          echo "I am mock Python!"
      fi
      """).split("\n")


# TODO(brandjon): Move this test to PY3. Blocked (ironically!) on the fix for
# #4815 being available in the host version of Bazel used to run this test.
class PywrapperTest(test_base.TestBase):
  """Unit tests for pywrapper_template.txt.

  These tests are based on the instantiation of the template for Python 2. They
  ensure that the wrapper can locate, validate, and launch a Python 2 executable
  on PATH. To ensure hermeticity, the tests launch the wrapper with PATH
  restricted to the scratch directory.

  Unix only.
  """

  def setup_tool(self, cmd):
    """Copies a command from its system location to the test directory."""
    path = shutil.which(cmd)
    self.assertIsNotNone(
        path, msg="Could not locate '%s' command on PATH" % cmd)
    # On recent MacOs versions, copying the coreutils tools elsewhere doesn't
    # work -- they simply fail with "Killed: 9". To workaround that, just
    # re-exec the actual binary.
    self.ScratchFile("dir/" + cmd,
                     ["#!/bin/sh", 'exec {} "$@"'.format(path)],
                     executable=True)

  def locate_runfile(self, runfile_path):
    resolved_path = self.Rlocation(runfile_path)
    self.assertIsNotNone(
        resolved_path, msg="Could not locate %s in runfiles" % runfile_path)
    return resolved_path

  def setUp(self):
    super(PywrapperTest, self).setUp()

    # Locate scripts under test.
    self.wrapper_path = self.locate_runfile(
        "io_bazel/tools/python/py3wrapper.sh"
    )
    self.nonstrict_wrapper_path = self.locate_runfile(
        "io_bazel/tools/python/py3wrapper_nonstrict.sh"
    )

    # Setup scratch directory with all executables the script depends on.
    #
    # This is brittle, but we need to make sure we can run the script when only
    # the scratch directory is on PATH, so that we can control whether or not
    # the python executables exist on PATH.
    self.setup_tool("which")
    self.setup_tool("echo")
    self.setup_tool("grep")

  def run_with_restricted_path(self, program, title_for_logging=None):
    new_env = dict(os.environ)
    new_env["PATH"] = self.Path("dir")
    proc = subprocess.Popen([program],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE,
                            universal_newlines=True,
                            cwd=self.Path("dir"),
                            env=new_env)
    # TODO(brandjon): Add a timeout arg here when upgraded to PY3.
    out, err = proc.communicate()
    if title_for_logging is not None:
      print(textwrap.dedent("""\
          ----------------
          %s
          Exit code: %d
          stdout:
          %s
          stderr:
          %s
          ----------------
          """) % (title_for_logging, proc.returncode, out, err))
    return proc.returncode, out, err

  def run_wrapper(self, title_for_logging):
    return self.run_with_restricted_path(self.wrapper_path, title_for_logging)

  def run_nonstrict_wrapper(self, title_for_logging):
    return self.run_with_restricted_path(self.nonstrict_wrapper_path,
                                         title_for_logging)

  def assert_wrapper_success(self, returncode, out, err):
    self.assertEqual(returncode, 0, msg="Expected to exit without error")
    self.assertEqual(
        out, "I am mock Python!\n", msg="stdout was not as expected")
    self.assertEqual(err, "", msg="Expected to produce no stderr output")

  def assert_wrapper_failure(self, returncode, out, err, message):
    self.assertEqual(returncode, 1, msg="Expected to exit with error code 1")
    self.assertRegex(
        err, message, msg="stderr did not contain expected string")

  def test_finds_python(self):
    self.ScratchFile("dir/python", MockPythonLines.NORMAL, executable=True)
    returncode, out, err = self.run_wrapper("test_finds_python")
    self.assert_wrapper_success(returncode, out, err)

  def test_no_interpreter_found(self):
    returncode, out, err = self.run_wrapper("test_no_interpreter_found")
    self.assert_wrapper_failure(
        returncode, out, err, "Neither 'python3' nor 'python' were found"
    )

  def test_error_getting_version(self):
    self.ScratchFile(
        "dir/python", MockPythonLines.VERSION_ERROR, executable=True
    )
    returncode, out, err = self.run_wrapper("test_error_getting_version")
    self.assert_wrapper_failure(returncode, out, err,
                                "Could not get interpreter version")

  def test_interpreter_not_executable(self):
    self.ScratchFile(
        "dir/python", MockPythonLines.VERSION_ERROR, executable=False
    )
    returncode, out, err = self.run_wrapper("test_interpreter_not_executable")
    self.assert_wrapper_failure(
        returncode, out, err, "Neither 'python3' nor 'python' were found"
    )


if __name__ == "__main__":
  unittest.main()
