blob: 9414a865c02b977a7fc86a19e3726c5b6303fae5 [file] [log] [blame]
# Copyright 2024 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 logging
import os
import os.path
import pathlib
import re
import shlex
import subprocess
import unittest
_logger = logging.getLogger(__name__)
class ExecuteError(Exception):
def __init__(self, result):
self.result = result
def __str__(self):
return self.result.describe()
class ExecuteResult:
def __init__(
self,
args: list[str],
env: dict[str, str],
cwd: pathlib.Path,
proc_result: subprocess.CompletedProcess,
):
self.args = args
self.env = env
self.cwd = cwd
self.exit_code = proc_result.returncode
self.stdout = proc_result.stdout
self.stderr = proc_result.stderr
def describe(self) -> str:
env_lines = [
" " + shlex.quote(f"{key}={value}")
for key, value in sorted(self.env.items())
]
env = " \\\n".join(env_lines)
args = shlex.join(self.args)
maybe_stdout_nl = "" if self.stdout.endswith("\n") else "\n"
maybe_stderr_nl = "" if self.stderr.endswith("\n") else "\n"
return f"""\
COMMAND:
cd {self.cwd} && \\
env \\
{env} \\
{args}
RESULT: exit_code: {self.exit_code}
===== STDOUT START =====
{self.stdout}{maybe_stdout_nl}===== STDOUT END =====
===== STDERR START =====
{self.stderr}{maybe_stderr_nl}===== STDERR END =====
"""
class TestCase(unittest.TestCase):
def setUp(self):
super().setUp()
self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"])
self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"])
outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"])
self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp"
# Put the global tmp not under the test tmp to better match how a real
# execution has entirely different directories for these.
self.tmp_dir = outer_test_tmpdir / "bit_tmp"
self.bazel_env = {
"PATH": os.environ["PATH"],
"TEST_TMPDIR": str(self.test_tmp_dir),
"TMP": str(self.tmp_dir),
# For some reason, this is necessary for Bazel 6.4 to work.
# If not present, it can't find some bash helpers in @bazel_tools
"RUNFILES_DIR": os.environ["TEST_SRCDIR"]
}
def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult:
"""Run a bazel invocation.
Args:
*args: The args to pass to bazel; the leading `bazel` command is
added automatically
check: True if the execution must succeed, False if failure
should raise an error.
Returns:
An `ExecuteResult` from running Bazel
"""
args = [str(self.bazel), *args]
env = self.bazel_env
_logger.info("executing: %s", shlex.join(args))
cwd = self.repo_root
proc_result = subprocess.run(
args=args,
text=True,
capture_output=True,
cwd=cwd,
env=env,
check=False,
)
exec_result = ExecuteResult(args, env, cwd, proc_result)
if check and exec_result.exit_code:
raise ExecuteError(exec_result)
else:
return exec_result
def assert_result_matches(self, result: ExecuteResult, regex: str) -> None:
"""Assert stdout/stderr of an invocation matches a regex.
Args:
result: ExecuteResult from `run_bazel` whose stdout/stderr will
be checked.
regex: Pattern to match, using `re.search` semantics.
"""
if not re.search(regex, result.stdout + result.stderr):
self.fail(
"Bazel output did not match expected pattern\n"
+ f"expected pattern: {regex}\n"
+ f"invocation details:\n{result.describe()}"
)