|  | # 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() |