blob: fa4123902ed08001717819be92b89a0eac7c694e [file] [log] [blame]
brandjon861a7e12019-04-25 08:40:31 -07001# pylint: disable=g-bad-file-header
2# Copyright 2019 The Bazel Authors. All rights reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16from __future__ import print_function
17
18import os
19import subprocess
20import textwrap
21import unittest
22
23from src.test.py.bazel import test_base
24
25
26class MockPythonLines(object):
27
28 NORMAL = textwrap.dedent(r"""\
29 if [ "$1" = "-V" ]; then
30 echo "Mock Python 2.xyz!"
31 else
32 echo "I am mock Python!"
33 fi
34 """).split("\n")
35
36 FAIL = textwrap.dedent(r"""\
37 echo "Mock failure!"
38 exit 1
39 """).split("\n")
40
41 WRONG_VERSION = textwrap.dedent(r"""\
42 if [ "$1" = "-V" ]; then
43 echo "Mock Python 3.xyz!"
44 else
45 echo "I am mock Python!"
46 fi
47 """).split("\n")
48
49 VERSION_ERROR = textwrap.dedent(r"""\
50 if [ "$1" = "-V" ]; then
51 echo "Error!"
52 exit 1
53 else
54 echo "I am mock Python!"
55 fi
56 """).split("\n")
57
58
59# TODO(brandjon): Switch to shutil.which when the test is moved to PY3.
60def which(cmd):
61 """A poor man's approximation of `shutil.which()` or the `which` command.
62
63 Args:
64 cmd: The command (executable) name to lookup; should not contain path
65 separators
66
67 Returns:
68 The absolute path to the first match in PATH, or None if not found.
69 """
70 for p in os.environ["PATH"].split(os.pathsep):
71 fullpath = os.path.abspath(os.path.join(p, cmd))
72 if os.path.exists(fullpath):
73 return fullpath
74 return None
75
76
77# TODO(brandjon): Move this test to PY3. Blocked (ironically!) on the fix for
78# #4815 being available in the host version of Bazel used to run this test.
79class PywrapperTest(test_base.TestBase):
80 """Unit tests for pywrapper_template.txt.
81
82 These tests are based on the instantiation of the template for Python 2. They
83 ensure that the wrapper can locate, validate, and launch a Python 2 executable
84 on PATH. To ensure hermeticity, the tests launch the wrapper with PATH
85 restricted to the scratch directory.
86
87 Unix only.
88 """
89
90 def setup_tool(self, cmd):
91 """Copies a command from its system location to the test directory."""
92 path = which(cmd)
93 self.assertIsNotNone(
94 path, msg="Could not locate '%s' command on PATH" % cmd)
95 self.CopyFile(path, os.path.join("dir", cmd), executable=True)
96
97 def setUp(self):
98 super(PywrapperTest, self).setUp()
99
100 # Locate script under test.
101 wrapper_path = self.Rlocation("io_bazel/tools/python/py2wrapper.sh")
102 self.assertIsNotNone(
103 wrapper_path, msg="Could not locate py2wrapper.sh in runfiles")
104 self.wrapper_path = wrapper_path
105
106 # Setup scratch directory with all executables the script depends on.
107 #
108 # This is brittle, but we need to make sure we can run the script when only
109 # the scratch directory is on PATH, so that we can control whether or not
110 # the python executables exist on PATH.
111 self.setup_tool("which")
112 self.setup_tool("echo")
113 self.setup_tool("grep")
114
115 def run_wrapper(self, title_for_logging=None):
116 new_env = dict(os.environ)
117 new_env["PATH"] = self.Path("dir")
118 proc = subprocess.Popen([self.wrapper_path],
119 stdout=subprocess.PIPE,
120 stderr=subprocess.PIPE,
121 universal_newlines=True,
122 cwd=self.Path("dir"),
123 env=new_env)
124 # TODO(brandjon): Add a timeout arg here when upgraded to PY3.
125 out, err = proc.communicate()
126 if title_for_logging is not None:
127 print(textwrap.dedent("""\
128 ----------------
129 %s
130 Exit code: %d
131 stdout:
132 %s
133 stderr:
134 %s
135 ----------------
136 """) % (title_for_logging, proc.returncode, out, err))
137 return proc.returncode, out, err
138
139 def assert_wrapper_success(self, returncode, out, err):
140 self.assertEqual(returncode, 0, msg="Expected to exit without error")
141 self.assertEqual(
142 out, "I am mock Python!\n", msg="stdout was not as expected")
143 self.assertEqual(err, "", msg="Expected to produce no stderr output")
144
145 def assert_wrapper_failure(self, returncode, out, err, message):
146 self.assertEqual(returncode, 1, msg="Expected to exit with error code 1")
147 self.assertRegexpMatches(
148 err, message, msg="stderr did not contain expected string")
149
150 def test_finds_python2(self):
151 self.ScratchFile("dir/python2", MockPythonLines.NORMAL, executable=True)
152 returncode, out, err = self.run_wrapper("test_finds_python2")
153 self.assert_wrapper_success(returncode, out, err)
154
155 def test_finds_python(self):
156 self.ScratchFile("dir/python", MockPythonLines.NORMAL, executable=True)
157 returncode, out, err = self.run_wrapper("test_finds_python")
158 self.assert_wrapper_success(returncode, out, err)
159
160 def test_prefers_python2(self):
161 self.ScratchFile("dir/python2", MockPythonLines.NORMAL, executable=True)
162 self.ScratchFile("dir/python", MockPythonLines.FAIL, executable=True)
163 returncode, out, err = self.run_wrapper("test_prefers_python2")
164 self.assert_wrapper_success(returncode, out, err)
165
166 def test_no_interpreter_found(self):
167 returncode, out, err = self.run_wrapper("test_no_interpreter_found")
168 self.assert_wrapper_failure(returncode, out, err,
169 "Neither 'python2' nor 'python' were found")
170
171 def test_wrong_version(self):
172 self.ScratchFile(
173 "dir/python2", MockPythonLines.WRONG_VERSION, executable=True)
174 returncode, out, err = self.run_wrapper("test_wrong_version")
175 self.assert_wrapper_failure(
176 returncode, out, err,
177 "version is 'Mock Python 3.xyz!', but we need version 2")
178
179 def test_error_getting_version(self):
180 self.ScratchFile(
181 "dir/python2", MockPythonLines.VERSION_ERROR, executable=True)
182 returncode, out, err = self.run_wrapper("test_error_getting_version")
183 self.assert_wrapper_failure(returncode, out, err,
184 "Could not get interpreter version")
185
186 def test_interpreter_not_executable(self):
187 self.ScratchFile(
188 "dir/python2", MockPythonLines.VERSION_ERROR, executable=False)
189 returncode, out, err = self.run_wrapper("test_interpreter_not_executable")
190 self.assert_wrapper_failure(returncode, out, err,
191 "Neither 'python2' nor 'python' were found")
192
193
194if __name__ == "__main__":
195 unittest.main()