-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Testing: add Python integration test utilities
This allows writing integration tests in Python rather than shell, so we can avoid #3148 This change also adds a test to verify that running "bazel info server_pid" twice yields the same PID. Again, this is testing that we indeed avoid the aformentioned bug. Change-Id: Ic800965b16ab87179370fd2cd43908286120e8d5 PiperOrigin-RevId: 158517192
- Loading branch information
1 parent
d32edba
commit e78ad83
Showing
4 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package(default_visibility = ["//visibility:private"]) | ||
|
||
filegroup( | ||
name = "srcs", | ||
srcs = glob(["**"]), | ||
visibility = ["//src:__pkg__"], | ||
) | ||
|
||
filegroup( | ||
name = "test-deps", | ||
testonly = 1, | ||
srcs = ["//src:bazel"], | ||
) | ||
|
||
py_library( | ||
name = "test_base", | ||
testonly = 1, | ||
srcs = ["test_base.py"], | ||
data = [":test-deps"], | ||
) | ||
|
||
py_test( | ||
name = "bazel_server_mode_test", | ||
size = "medium", | ||
srcs = ["bazel_server_mode_test.py"], | ||
deps = [":test_base"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
# Copyright 2017 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. | ||
|
||
import unittest | ||
|
||
from src.test.py.bazel import test_base | ||
|
||
|
||
class BazelCleanTest(test_base.TestBase): | ||
|
||
def testBazelClean(self): | ||
self.ScratchFile('WORKSPACE') | ||
|
||
exit_code, stdout, _ = self.RunBazel(['info', 'server_pid']) | ||
self.assertEqual(exit_code, 0) | ||
pid1 = stdout[0] | ||
|
||
exit_code, stdout, _ = self.RunBazel(['info', 'server_pid']) | ||
self.assertEqual(exit_code, 0) | ||
pid2 = stdout[0] | ||
|
||
self.assertEqual(pid1, pid2) | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
# Copyright 2017 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. | ||
|
||
import os | ||
import subprocess | ||
import sys | ||
import unittest | ||
|
||
|
||
class Error(Exception): | ||
"""Base class for errors in this module.""" | ||
pass | ||
|
||
|
||
class ArgumentError(Error): | ||
"""A function received a bad argument.""" | ||
pass | ||
|
||
|
||
class EnvVarUndefinedError(Error): | ||
"""An expected environment variable is not defined.""" | ||
|
||
def __init__(self, name): | ||
Error.__init__(self, 'Environment variable "%s" is not defined' % name) | ||
|
||
|
||
class TestBase(unittest.TestCase): | ||
|
||
_stderr = None | ||
_stdout = None | ||
_runfiles = None | ||
_temp = None | ||
_tests_root = None | ||
|
||
def setUp(self): | ||
unittest.TestCase.setUp(self) | ||
if self._runfiles is None: | ||
self._runfiles = TestBase._LoadRunfiles() | ||
test_tmpdir = TestBase.GetEnv('TEST_TMPDIR') | ||
self._stdout = os.path.join(test_tmpdir, 'bazel.stdout') | ||
self._stderr = os.path.join(test_tmpdir, 'bazel.stderr') | ||
self._temp = os.path.join(test_tmpdir, 'tmp') | ||
self._tests_root = os.path.join(test_tmpdir, 'tests_root') | ||
os.mkdir(self._tests_root) | ||
os.chdir(self._tests_root) | ||
|
||
@staticmethod | ||
def GetEnv(name, default=None): | ||
"""Returns environment variable `name`. | ||
Args: | ||
name: string; name of the environment variable | ||
default: anything; return this value if the envvar is not defined | ||
Returns: | ||
string, the envvar's value if defined, or `default` if the envvar is not | ||
defined but `default` is | ||
Raises: | ||
EnvVarUndefinedError: if `name` is not a defined envvar and `default` is | ||
None | ||
""" | ||
value = os.getenv(name, '__undefined_envvar__') | ||
if value == '__undefined_envvar__': | ||
if default: | ||
return default | ||
raise EnvVarUndefinedError(name) | ||
return value | ||
|
||
@staticmethod | ||
def IsWindows(): | ||
"""Returns true if the current platform is Windows.""" | ||
return os.name == 'nt' | ||
|
||
def Path(self, path): | ||
"""Returns the absolute path of `path` relative to the scratch directory. | ||
Args: | ||
path: string; a path, relative to the test's scratch directory, | ||
e.g. "foo/bar/BUILD" | ||
Returns: | ||
an absolute path | ||
Raises: | ||
ArgumentError: if `path` is absolute or contains uplevel references | ||
""" | ||
if os.path.isabs(path) or '..' in path: | ||
raise ArgumentError(('path="%s" may not be absolute and may not contain ' | ||
'uplevel references') % path) | ||
return os.path.join(self._tests_root, path) | ||
|
||
def Rlocation(self, runfile): | ||
"""Returns the absolute path to a runfile.""" | ||
if TestBase.IsWindows(): | ||
return self._runfiles.get(runfile) | ||
else: | ||
return os.path.join(self._runfiles, runfile) | ||
|
||
def ScratchDir(self, path): | ||
"""Creates directories under the test's scratch directory. | ||
Args: | ||
path: string; a path, relative to the test's scratch directory, | ||
e.g. "foo/bar" | ||
Raises: | ||
ArgumentError: if `path` is absolute or contains uplevel references | ||
IOError: if an I/O error occurs | ||
""" | ||
if not path: | ||
return | ||
abspath = self.Path(path) | ||
if os.path.exists(abspath) and not os.path.isdir(abspath): | ||
raise IOError('"%s" (%s) exists and is not a directory' % (path, abspath)) | ||
os.makedirs(abspath) | ||
|
||
def ScratchFile(self, path, lines=None): | ||
"""Creates a file under the test's scratch directory. | ||
Args: | ||
path: string; a path, relative to the test's scratch directory, | ||
e.g. "foo/bar/BUILD" | ||
lines: [string]; the contents of the file (newlines are added | ||
automatically) | ||
Raises: | ||
ArgumentError: if `path` is absolute or contains uplevel references | ||
IOError: if an I/O error occurs | ||
""" | ||
if not path: | ||
return | ||
abspath = self.Path(path) | ||
if os.path.exists(abspath) and not os.path.isfile(abspath): | ||
raise IOError('"%s" (%s) exists and is not a file' % (path, abspath)) | ||
self.ScratchDir(os.path.dirname(path)) | ||
with open(abspath, 'w') as f: | ||
if lines: | ||
for l in lines: | ||
f.write(l) | ||
f.write('\n') | ||
|
||
def RunBazel(self, args): | ||
"""Runs "bazel <args>", waits for it to exit. | ||
Args: | ||
args: [string]; flags to pass to bazel (e.g. ['--batch', 'build', '//x']) | ||
Returns: | ||
(int, [string], [string]) tuple: exit code, stdout lines, stderr lines | ||
""" | ||
with open(self._stdout, 'w') as stdout: | ||
with open(self._stderr, 'w') as stderr: | ||
proc = subprocess.Popen( | ||
[ | ||
self.Rlocation('io_bazel/src/bazel'), '--bazelrc=/dev/null', | ||
'--nomaster_bazelrc' | ||
] + args, | ||
stdout=stdout, | ||
stderr=stderr, | ||
cwd=self._tests_root, | ||
env=self._BazelEnvMap()) | ||
exit_code = proc.wait() | ||
|
||
with open(self._stdout, 'r') as f: | ||
stdout = [l.strip() for l in f.readlines()] | ||
with open(self._stderr, 'r') as f: | ||
stderr = [l.strip() for l in f.readlines()] | ||
return exit_code, stdout, stderr | ||
|
||
def _BazelEnvMap(self): | ||
"""Returns the environment variable map to run Bazel.""" | ||
if TestBase.IsWindows(): | ||
result = [] | ||
if sys.version_info.major == 3: | ||
# Python 3.2 has os.listdir | ||
result = [ | ||
n for n in os.listdir('c:\\program files\\java') | ||
if n.startswith('jdk') | ||
] | ||
else: | ||
# Python 2.7 has os.path.walk | ||
def _Visit(result, _, names): | ||
result.extend(n for n in names if n.startswith('jdk')) | ||
while names: | ||
names.pop() | ||
|
||
os.path.walk('c:\\program files\\java\\', _Visit, result) | ||
env = { | ||
'SYSTEMROOT': TestBase.GetEnv('SYSTEMROOT'), | ||
# TODO(laszlocsomor): Let Bazel pass BAZEL_SH and JAVA_HOME to tests | ||
# and use those here instead of hardcoding paths. | ||
'JAVA_HOME': 'c:\\program files\\java\\' + sorted(result)[-1], | ||
'BAZEL_SH': 'c:\\tools\\msys64\\usr\\bin\\bash.exe' | ||
} | ||
else: | ||
env = {'HOME': os.path.join(self._temp, 'home')} | ||
|
||
env['PATH'] = TestBase.GetEnv('PATH') | ||
# The inner Bazel must know that it's running as part of a test (so that it | ||
# uses --max_idle_secs=15 by default instead of 3 hours, etc.), and it knows | ||
# that by checking for TEST_TMPDIR. | ||
env['TEST_TMPDIR'] = TestBase.GetEnv('TEST_TMPDIR') | ||
env['TMP'] = self._temp | ||
return env | ||
|
||
@staticmethod | ||
def _LoadRunfiles(): | ||
"""Loads the runfiles manifest from ${TEST_SRCDIR}/MANIFEST. | ||
Only necessary to use on Windows, where runfiles are not symlinked in to the | ||
runfiles directory, but are written to a MANIFEST file instead. | ||
Returns: | ||
on Windows: {string: string} dictionary, keys are runfiles-relative paths, | ||
values are absolute paths that the runfiles entry is mapped to; | ||
on other platforms: string; value of $TEST_SRCDIR | ||
""" | ||
test_srcdir = TestBase.GetEnv('TEST_SRCDIR') | ||
if not TestBase.IsWindows(): | ||
return test_srcdir | ||
|
||
result = {} | ||
with open(os.path.join(test_srcdir, 'MANIFEST'), 'r') as f: | ||
for l in f: | ||
tokens = l.strip().split(' ') | ||
if len(tokens) == 2: | ||
result[tokens[0]] = tokens[1] | ||
return result |