# 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()}" )