| # 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 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): Switch to shutil.which when the test is moved to PY3. |
| def which(cmd): |
| """A poor man's approximation of `shutil.which()` or the `which` command. |
| |
| Args: |
| cmd: The command (executable) name to lookup; should not contain path |
| separators |
| |
| Returns: |
| The absolute path to the first match in PATH, or None if not found. |
| """ |
| for p in os.environ["PATH"].split(os.pathsep): |
| fullpath = os.path.abspath(os.path.join(p, cmd)) |
| if os.path.exists(fullpath): |
| return fullpath |
| return None |
| |
| |
| # 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 = 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() |