1*60517a1eSAndroid Build Coastguard Worker# Copyright 2024 The Bazel Authors. All rights reserved. 2*60517a1eSAndroid Build Coastguard Worker# 3*60517a1eSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 4*60517a1eSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 5*60517a1eSAndroid Build Coastguard Worker# You may obtain a copy of the License at 6*60517a1eSAndroid Build Coastguard Worker# 7*60517a1eSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 8*60517a1eSAndroid Build Coastguard Worker# 9*60517a1eSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 10*60517a1eSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 11*60517a1eSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12*60517a1eSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 13*60517a1eSAndroid Build Coastguard Worker# limitations under the License. 14*60517a1eSAndroid Build Coastguard Worker 15*60517a1eSAndroid Build Coastguard Workerimport logging 16*60517a1eSAndroid Build Coastguard Workerimport os 17*60517a1eSAndroid Build Coastguard Workerimport os.path 18*60517a1eSAndroid Build Coastguard Workerimport pathlib 19*60517a1eSAndroid Build Coastguard Workerimport re 20*60517a1eSAndroid Build Coastguard Workerimport shlex 21*60517a1eSAndroid Build Coastguard Workerimport subprocess 22*60517a1eSAndroid Build Coastguard Workerimport unittest 23*60517a1eSAndroid Build Coastguard Worker 24*60517a1eSAndroid Build Coastguard Worker_logger = logging.getLogger(__name__) 25*60517a1eSAndroid Build Coastguard Worker 26*60517a1eSAndroid Build Coastguard Workerclass ExecuteError(Exception): 27*60517a1eSAndroid Build Coastguard Worker def __init__(self, result): 28*60517a1eSAndroid Build Coastguard Worker self.result = result 29*60517a1eSAndroid Build Coastguard Worker def __str__(self): 30*60517a1eSAndroid Build Coastguard Worker return self.result.describe() 31*60517a1eSAndroid Build Coastguard Worker 32*60517a1eSAndroid Build Coastguard Workerclass ExecuteResult: 33*60517a1eSAndroid Build Coastguard Worker def __init__( 34*60517a1eSAndroid Build Coastguard Worker self, 35*60517a1eSAndroid Build Coastguard Worker args: list[str], 36*60517a1eSAndroid Build Coastguard Worker env: dict[str, str], 37*60517a1eSAndroid Build Coastguard Worker cwd: pathlib.Path, 38*60517a1eSAndroid Build Coastguard Worker proc_result: subprocess.CompletedProcess, 39*60517a1eSAndroid Build Coastguard Worker ): 40*60517a1eSAndroid Build Coastguard Worker self.args = args 41*60517a1eSAndroid Build Coastguard Worker self.env = env 42*60517a1eSAndroid Build Coastguard Worker self.cwd = cwd 43*60517a1eSAndroid Build Coastguard Worker self.exit_code = proc_result.returncode 44*60517a1eSAndroid Build Coastguard Worker self.stdout = proc_result.stdout 45*60517a1eSAndroid Build Coastguard Worker self.stderr = proc_result.stderr 46*60517a1eSAndroid Build Coastguard Worker 47*60517a1eSAndroid Build Coastguard Worker def describe(self) -> str: 48*60517a1eSAndroid Build Coastguard Worker env_lines = [ 49*60517a1eSAndroid Build Coastguard Worker " " + shlex.quote(f"{key}={value}") 50*60517a1eSAndroid Build Coastguard Worker for key, value in sorted(self.env.items()) 51*60517a1eSAndroid Build Coastguard Worker ] 52*60517a1eSAndroid Build Coastguard Worker env = " \\\n".join(env_lines) 53*60517a1eSAndroid Build Coastguard Worker args = shlex.join(self.args) 54*60517a1eSAndroid Build Coastguard Worker maybe_stdout_nl = "" if self.stdout.endswith("\n") else "\n" 55*60517a1eSAndroid Build Coastguard Worker maybe_stderr_nl = "" if self.stderr.endswith("\n") else "\n" 56*60517a1eSAndroid Build Coastguard Worker return f"""\ 57*60517a1eSAndroid Build Coastguard WorkerCOMMAND: 58*60517a1eSAndroid Build Coastguard Workercd {self.cwd} && \\ 59*60517a1eSAndroid Build Coastguard Workerenv \\ 60*60517a1eSAndroid Build Coastguard Worker{env} \\ 61*60517a1eSAndroid Build Coastguard Worker {args} 62*60517a1eSAndroid Build Coastguard WorkerRESULT: exit_code: {self.exit_code} 63*60517a1eSAndroid Build Coastguard Worker===== STDOUT START ===== 64*60517a1eSAndroid Build Coastguard Worker{self.stdout}{maybe_stdout_nl}===== STDOUT END ===== 65*60517a1eSAndroid Build Coastguard Worker===== STDERR START ===== 66*60517a1eSAndroid Build Coastguard Worker{self.stderr}{maybe_stderr_nl}===== STDERR END ===== 67*60517a1eSAndroid Build Coastguard Worker""" 68*60517a1eSAndroid Build Coastguard Worker 69*60517a1eSAndroid Build Coastguard Worker 70*60517a1eSAndroid Build Coastguard Workerclass TestCase(unittest.TestCase): 71*60517a1eSAndroid Build Coastguard Worker def setUp(self): 72*60517a1eSAndroid Build Coastguard Worker super().setUp() 73*60517a1eSAndroid Build Coastguard Worker self.repo_root = pathlib.Path(os.environ["BIT_WORKSPACE_DIR"]) 74*60517a1eSAndroid Build Coastguard Worker self.bazel = pathlib.Path(os.environ["BIT_BAZEL_BINARY"]) 75*60517a1eSAndroid Build Coastguard Worker outer_test_tmpdir = pathlib.Path(os.environ["TEST_TMPDIR"]) 76*60517a1eSAndroid Build Coastguard Worker self.test_tmp_dir = outer_test_tmpdir / "bit_test_tmp" 77*60517a1eSAndroid Build Coastguard Worker # Put the global tmp not under the test tmp to better match how a real 78*60517a1eSAndroid Build Coastguard Worker # execution has entirely different directories for these. 79*60517a1eSAndroid Build Coastguard Worker self.tmp_dir = outer_test_tmpdir / "bit_tmp" 80*60517a1eSAndroid Build Coastguard Worker self.bazel_env = { 81*60517a1eSAndroid Build Coastguard Worker "PATH": os.environ["PATH"], 82*60517a1eSAndroid Build Coastguard Worker "TEST_TMPDIR": str(self.test_tmp_dir), 83*60517a1eSAndroid Build Coastguard Worker "TMP": str(self.tmp_dir), 84*60517a1eSAndroid Build Coastguard Worker # For some reason, this is necessary for Bazel 6.4 to work. 85*60517a1eSAndroid Build Coastguard Worker # If not present, it can't find some bash helpers in @bazel_tools 86*60517a1eSAndroid Build Coastguard Worker "RUNFILES_DIR": os.environ["TEST_SRCDIR"] 87*60517a1eSAndroid Build Coastguard Worker } 88*60517a1eSAndroid Build Coastguard Worker 89*60517a1eSAndroid Build Coastguard Worker def run_bazel(self, *args: str, check: bool = True) -> ExecuteResult: 90*60517a1eSAndroid Build Coastguard Worker """Run a bazel invocation. 91*60517a1eSAndroid Build Coastguard Worker 92*60517a1eSAndroid Build Coastguard Worker Args: 93*60517a1eSAndroid Build Coastguard Worker *args: The args to pass to bazel; the leading `bazel` command is 94*60517a1eSAndroid Build Coastguard Worker added automatically 95*60517a1eSAndroid Build Coastguard Worker check: True if the execution must succeed, False if failure 96*60517a1eSAndroid Build Coastguard Worker should raise an error. 97*60517a1eSAndroid Build Coastguard Worker Returns: 98*60517a1eSAndroid Build Coastguard Worker An `ExecuteResult` from running Bazel 99*60517a1eSAndroid Build Coastguard Worker """ 100*60517a1eSAndroid Build Coastguard Worker args = [str(self.bazel), *args] 101*60517a1eSAndroid Build Coastguard Worker env = self.bazel_env 102*60517a1eSAndroid Build Coastguard Worker _logger.info("executing: %s", shlex.join(args)) 103*60517a1eSAndroid Build Coastguard Worker cwd = self.repo_root 104*60517a1eSAndroid Build Coastguard Worker proc_result = subprocess.run( 105*60517a1eSAndroid Build Coastguard Worker args=args, 106*60517a1eSAndroid Build Coastguard Worker text=True, 107*60517a1eSAndroid Build Coastguard Worker capture_output=True, 108*60517a1eSAndroid Build Coastguard Worker cwd=cwd, 109*60517a1eSAndroid Build Coastguard Worker env=env, 110*60517a1eSAndroid Build Coastguard Worker check=False, 111*60517a1eSAndroid Build Coastguard Worker ) 112*60517a1eSAndroid Build Coastguard Worker exec_result = ExecuteResult(args, env, cwd, proc_result) 113*60517a1eSAndroid Build Coastguard Worker if check and exec_result.exit_code: 114*60517a1eSAndroid Build Coastguard Worker raise ExecuteError(exec_result) 115*60517a1eSAndroid Build Coastguard Worker else: 116*60517a1eSAndroid Build Coastguard Worker return exec_result 117*60517a1eSAndroid Build Coastguard Worker 118*60517a1eSAndroid Build Coastguard Worker def assert_result_matches(self, result: ExecuteResult, regex: str) -> None: 119*60517a1eSAndroid Build Coastguard Worker """Assert stdout/stderr of an invocation matches a regex. 120*60517a1eSAndroid Build Coastguard Worker 121*60517a1eSAndroid Build Coastguard Worker Args: 122*60517a1eSAndroid Build Coastguard Worker result: ExecuteResult from `run_bazel` whose stdout/stderr will 123*60517a1eSAndroid Build Coastguard Worker be checked. 124*60517a1eSAndroid Build Coastguard Worker regex: Pattern to match, using `re.search` semantics. 125*60517a1eSAndroid Build Coastguard Worker """ 126*60517a1eSAndroid Build Coastguard Worker if not re.search(regex, result.stdout + result.stderr): 127*60517a1eSAndroid Build Coastguard Worker self.fail( 128*60517a1eSAndroid Build Coastguard Worker "Bazel output did not match expected pattern\n" 129*60517a1eSAndroid Build Coastguard Worker + f"expected pattern: {regex}\n" 130*60517a1eSAndroid Build Coastguard Worker + f"invocation details:\n{result.describe()}" 131*60517a1eSAndroid Build Coastguard Worker ) 132