Skip to content

Commit

Permalink
Testing: add Python integration test utilities
Browse files Browse the repository at this point in the history
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
laszlocsomor authored and katre committed Jun 9, 2017
1 parent d32edba commit e78ad83
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ filegroup(
"//src/test/java/com/google/devtools/build/lib:srcs",
"//src/test/java/com/google/devtools/build/skyframe:srcs",
"//src/test/java/com/google/devtools/common/options:srcs",
"//src/test/py/bazel:srcs",
"//src/test/shell:srcs",
"//src/tools/android/java/com/google/devtools/build/android:srcs",
"//src/tools/benchmark:srcs",
Expand Down
27 changes: 27 additions & 0 deletions src/test/py/bazel/BUILD
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"],
)
37 changes: 37 additions & 0 deletions src/test/py/bazel/bazel_server_mode_test.py
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()
233 changes: 233 additions & 0 deletions src/test/py/bazel/test_base.py
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

0 comments on commit e78ad83

Please sign in to comment.