blob: a014d7ac3dfeac8f467a3113a6bd732a46f608dd [file] [log] [blame] [edit]
# 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()