blob: 07732a627988e3ec37c974c4129d93b73d6b7781 [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
brandjon052167e2019-06-04 16:04:06 -070097 def locate_runfile(self, runfile_path):
98 resolved_path = self.Rlocation(runfile_path)
99 self.assertIsNotNone(
100 resolved_path, msg="Could not locate %s in runfiles" % runfile_path)
101 return resolved_path
102
brandjon861a7e12019-04-25 08:40:31 -0700103 def setUp(self):
104 super(PywrapperTest, self).setUp()
105
brandjon052167e2019-06-04 16:04:06 -0700106 # Locate scripts under test.
107 self.wrapper_path = \
108 self.locate_runfile("io_bazel/tools/python/py2wrapper.sh")
109 self.nonstrict_wrapper_path = \
110 self.locate_runfile("io_bazel/tools/python/py2wrapper_nonstrict.sh")
brandjon861a7e12019-04-25 08:40:31 -0700111
112 # Setup scratch directory with all executables the script depends on.
113 #
114 # This is brittle, but we need to make sure we can run the script when only
115 # the scratch directory is on PATH, so that we can control whether or not
116 # the python executables exist on PATH.
117 self.setup_tool("which")
118 self.setup_tool("echo")
119 self.setup_tool("grep")
120
brandjon052167e2019-06-04 16:04:06 -0700121 def run_with_restricted_path(self, program, title_for_logging=None):
brandjon861a7e12019-04-25 08:40:31 -0700122 new_env = dict(os.environ)
123 new_env["PATH"] = self.Path("dir")
brandjon052167e2019-06-04 16:04:06 -0700124 proc = subprocess.Popen([program],
brandjon861a7e12019-04-25 08:40:31 -0700125 stdout=subprocess.PIPE,
126 stderr=subprocess.PIPE,
127 universal_newlines=True,
128 cwd=self.Path("dir"),
129 env=new_env)
130 # TODO(brandjon): Add a timeout arg here when upgraded to PY3.
131 out, err = proc.communicate()
132 if title_for_logging is not None:
133 print(textwrap.dedent("""\
134 ----------------
135 %s
136 Exit code: %d
137 stdout:
138 %s
139 stderr:
140 %s
141 ----------------
142 """) % (title_for_logging, proc.returncode, out, err))
143 return proc.returncode, out, err
144
brandjon052167e2019-06-04 16:04:06 -0700145 def run_wrapper(self, title_for_logging):
146 return self.run_with_restricted_path(self.wrapper_path, title_for_logging)
147
148 def run_nonstrict_wrapper(self, title_for_logging):
149 return self.run_with_restricted_path(self.nonstrict_wrapper_path,
150 title_for_logging)
151
brandjon861a7e12019-04-25 08:40:31 -0700152 def assert_wrapper_success(self, returncode, out, err):
153 self.assertEqual(returncode, 0, msg="Expected to exit without error")
154 self.assertEqual(
155 out, "I am mock Python!\n", msg="stdout was not as expected")
156 self.assertEqual(err, "", msg="Expected to produce no stderr output")
157
158 def assert_wrapper_failure(self, returncode, out, err, message):
159 self.assertEqual(returncode, 1, msg="Expected to exit with error code 1")
160 self.assertRegexpMatches(
161 err, message, msg="stderr did not contain expected string")
162
163 def test_finds_python2(self):
164 self.ScratchFile("dir/python2", MockPythonLines.NORMAL, executable=True)
165 returncode, out, err = self.run_wrapper("test_finds_python2")
166 self.assert_wrapper_success(returncode, out, err)
167
168 def test_finds_python(self):
169 self.ScratchFile("dir/python", MockPythonLines.NORMAL, executable=True)
170 returncode, out, err = self.run_wrapper("test_finds_python")
171 self.assert_wrapper_success(returncode, out, err)
172
173 def test_prefers_python2(self):
174 self.ScratchFile("dir/python2", MockPythonLines.NORMAL, executable=True)
175 self.ScratchFile("dir/python", MockPythonLines.FAIL, executable=True)
176 returncode, out, err = self.run_wrapper("test_prefers_python2")
177 self.assert_wrapper_success(returncode, out, err)
178
179 def test_no_interpreter_found(self):
180 returncode, out, err = self.run_wrapper("test_no_interpreter_found")
181 self.assert_wrapper_failure(returncode, out, err,
182 "Neither 'python2' nor 'python' were found")
183
184 def test_wrong_version(self):
185 self.ScratchFile(
186 "dir/python2", MockPythonLines.WRONG_VERSION, executable=True)
187 returncode, out, err = self.run_wrapper("test_wrong_version")
188 self.assert_wrapper_failure(
189 returncode, out, err,
190 "version is 'Mock Python 3.xyz!', but we need version 2")
191
192 def test_error_getting_version(self):
193 self.ScratchFile(
194 "dir/python2", MockPythonLines.VERSION_ERROR, executable=True)
195 returncode, out, err = self.run_wrapper("test_error_getting_version")
196 self.assert_wrapper_failure(returncode, out, err,
197 "Could not get interpreter version")
198
199 def test_interpreter_not_executable(self):
200 self.ScratchFile(
201 "dir/python2", MockPythonLines.VERSION_ERROR, executable=False)
202 returncode, out, err = self.run_wrapper("test_interpreter_not_executable")
203 self.assert_wrapper_failure(returncode, out, err,
204 "Neither 'python2' nor 'python' were found")
205
brandjon052167e2019-06-04 16:04:06 -0700206 def test_wrong_version_ok_for_nonstrict(self):
207 self.ScratchFile(
208 "dir/python2", MockPythonLines.WRONG_VERSION, executable=True)
209 returncode, out, err = \
210 self.run_nonstrict_wrapper("test_wrong_version_ok_for_nonstrict")
211 self.assert_wrapper_success(returncode, out, err)
212
brandjon861a7e12019-04-25 08:40:31 -0700213
214if __name__ == "__main__":
215 unittest.main()