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