blob: baee22c7b99ca288b5304b8a96b6f4dfad2b43ab [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
Richard Levasseurbcb1fea2023-01-23 14:44:45 -080028 NORMAL = textwrap.dedent(
29 r"""\
brandjon861a7e12019-04-25 08:40:31 -070030 if [ "$1" = "-V" ]; then
31 echo "Mock Python 3.xyz!"
32 else
33 echo "I am mock Python!"
34 fi
Richard Levasseurbcb1fea2023-01-23 14:44:45 -080035 """
36 ).split("\n")
37
38 FAIL = textwrap.dedent(r"""\
39 echo "Mock failure!"
40 exit 1
brandjon861a7e12019-04-25 08:40:31 -070041 """).split("\n")
42
43 VERSION_ERROR = textwrap.dedent(r"""\
44 if [ "$1" = "-V" ]; then
45 echo "Error!"
46 exit 1
47 else
48 echo "I am mock Python!"
49 fi
50 """).split("\n")
51
52
53# TODO(brandjon): Switch to shutil.which when the test is moved to PY3.
54def which(cmd):
55 """A poor man's approximation of `shutil.which()` or the `which` command.
56
57 Args:
58 cmd: The command (executable) name to lookup; should not contain path
59 separators
60
61 Returns:
62 The absolute path to the first match in PATH, or None if not found.
63 """
64 for p in os.environ["PATH"].split(os.pathsep):
65 fullpath = os.path.abspath(os.path.join(p, cmd))
66 if os.path.exists(fullpath):
67 return fullpath
68 return None
69
70
71# TODO(brandjon): Move this test to PY3. Blocked (ironically!) on the fix for
72# #4815 being available in the host version of Bazel used to run this test.
73class PywrapperTest(test_base.TestBase):
74 """Unit tests for pywrapper_template.txt.
75
76 These tests are based on the instantiation of the template for Python 2. They
77 ensure that the wrapper can locate, validate, and launch a Python 2 executable
78 on PATH. To ensure hermeticity, the tests launch the wrapper with PATH
79 restricted to the scratch directory.
80
81 Unix only.
82 """
83
84 def setup_tool(self, cmd):
85 """Copies a command from its system location to the test directory."""
86 path = which(cmd)
87 self.assertIsNotNone(
88 path, msg="Could not locate '%s' command on PATH" % cmd)
Richard Levasseure9316f02023-02-06 10:56:29 -080089 # On recent MacOs versions, copying the coreutils tools elsewhere doesn't
90 # work -- they simply fail with "Killed: 9". To workaround that, just
91 # re-exec the actual binary.
92 self.ScratchFile("dir/" + cmd,
Richard Levasseurb2b70392023-02-06 17:51:05 -080093 ["#!/bin/sh", 'exec {} "$@"'.format(path)],
Richard Levasseure9316f02023-02-06 10:56:29 -080094 executable=True)
brandjon861a7e12019-04-25 08:40:31 -070095
brandjon052167e2019-06-04 16:04:06 -070096 def locate_runfile(self, runfile_path):
97 resolved_path = self.Rlocation(runfile_path)
98 self.assertIsNotNone(
99 resolved_path, msg="Could not locate %s in runfiles" % runfile_path)
100 return resolved_path
101
brandjon861a7e12019-04-25 08:40:31 -0700102 def setUp(self):
103 super(PywrapperTest, self).setUp()
104
brandjon052167e2019-06-04 16:04:06 -0700105 # Locate scripts under test.
Richard Levasseurbcb1fea2023-01-23 14:44:45 -0800106 self.wrapper_path = self.locate_runfile(
107 "io_bazel/tools/python/py3wrapper.sh"
108 )
109 self.nonstrict_wrapper_path = self.locate_runfile(
110 "io_bazel/tools/python/py3wrapper_nonstrict.sh"
111 )
brandjon861a7e12019-04-25 08:40:31 -0700112
113 # Setup scratch directory with all executables the script depends on.
114 #
115 # This is brittle, but we need to make sure we can run the script when only
116 # the scratch directory is on PATH, so that we can control whether or not
117 # the python executables exist on PATH.
118 self.setup_tool("which")
119 self.setup_tool("echo")
120 self.setup_tool("grep")
121
brandjon052167e2019-06-04 16:04:06 -0700122 def run_with_restricted_path(self, program, title_for_logging=None):
brandjon861a7e12019-04-25 08:40:31 -0700123 new_env = dict(os.environ)
124 new_env["PATH"] = self.Path("dir")
brandjon052167e2019-06-04 16:04:06 -0700125 proc = subprocess.Popen([program],
brandjon861a7e12019-04-25 08:40:31 -0700126 stdout=subprocess.PIPE,
127 stderr=subprocess.PIPE,
128 universal_newlines=True,
129 cwd=self.Path("dir"),
130 env=new_env)
131 # TODO(brandjon): Add a timeout arg here when upgraded to PY3.
132 out, err = proc.communicate()
133 if title_for_logging is not None:
134 print(textwrap.dedent("""\
135 ----------------
136 %s
137 Exit code: %d
138 stdout:
139 %s
140 stderr:
141 %s
142 ----------------
143 """) % (title_for_logging, proc.returncode, out, err))
144 return proc.returncode, out, err
145
brandjon052167e2019-06-04 16:04:06 -0700146 def run_wrapper(self, title_for_logging):
147 return self.run_with_restricted_path(self.wrapper_path, title_for_logging)
148
149 def run_nonstrict_wrapper(self, title_for_logging):
150 return self.run_with_restricted_path(self.nonstrict_wrapper_path,
151 title_for_logging)
152
brandjon861a7e12019-04-25 08:40:31 -0700153 def assert_wrapper_success(self, returncode, out, err):
154 self.assertEqual(returncode, 0, msg="Expected to exit without error")
155 self.assertEqual(
156 out, "I am mock Python!\n", msg="stdout was not as expected")
157 self.assertEqual(err, "", msg="Expected to produce no stderr output")
158
159 def assert_wrapper_failure(self, returncode, out, err, message):
160 self.assertEqual(returncode, 1, msg="Expected to exit with error code 1")
161 self.assertRegexpMatches(
162 err, message, msg="stderr did not contain expected string")
163
brandjon861a7e12019-04-25 08:40:31 -0700164 def test_finds_python(self):
165 self.ScratchFile("dir/python", MockPythonLines.NORMAL, executable=True)
166 returncode, out, err = self.run_wrapper("test_finds_python")
167 self.assert_wrapper_success(returncode, out, err)
168
brandjon861a7e12019-04-25 08:40:31 -0700169 def test_no_interpreter_found(self):
170 returncode, out, err = self.run_wrapper("test_no_interpreter_found")
brandjon861a7e12019-04-25 08:40:31 -0700171 self.assert_wrapper_failure(
Richard Levasseurbcb1fea2023-01-23 14:44:45 -0800172 returncode, out, err, "Neither 'python3' nor 'python' were found"
173 )
brandjon861a7e12019-04-25 08:40:31 -0700174
175 def test_error_getting_version(self):
176 self.ScratchFile(
Richard Levasseurbcb1fea2023-01-23 14:44:45 -0800177 "dir/python", MockPythonLines.VERSION_ERROR, executable=True
178 )
brandjon861a7e12019-04-25 08:40:31 -0700179 returncode, out, err = self.run_wrapper("test_error_getting_version")
180 self.assert_wrapper_failure(returncode, out, err,
181 "Could not get interpreter version")
182
183 def test_interpreter_not_executable(self):
184 self.ScratchFile(
Richard Levasseurbcb1fea2023-01-23 14:44:45 -0800185 "dir/python", MockPythonLines.VERSION_ERROR, executable=False
186 )
brandjon861a7e12019-04-25 08:40:31 -0700187 returncode, out, err = self.run_wrapper("test_interpreter_not_executable")
Richard Levasseurbcb1fea2023-01-23 14:44:45 -0800188 self.assert_wrapper_failure(
189 returncode, out, err, "Neither 'python3' nor 'python' were found"
190 )
brandjon052167e2019-06-04 16:04:06 -0700191
brandjon861a7e12019-04-25 08:40:31 -0700192
193if __name__ == "__main__":
194 unittest.main()