1# Copyright 2021, The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15""" 16Implementation of Atest's Bazel mode. 17 18Bazel mode runs tests using Bazel by generating a synthetic workspace that 19contains test targets. Using Bazel allows Atest to leverage features such as 20sandboxing, caching, and remote execution. 21""" 22# pylint: disable=missing-function-docstring 23# pylint: disable=missing-class-docstring 24# pylint: disable=too-many-lines 25 26from __future__ import annotations 27 28import argparse 29import contextlib 30import dataclasses 31import enum 32import functools 33import os 34import re 35import shutil 36import subprocess 37import warnings 38 39from abc import ABC, abstractmethod 40from collections import defaultdict, deque, OrderedDict 41from pathlib import Path 42from types import MappingProxyType 43from typing import Any, Callable, Dict, IO, List, Set 44 45import atest_utils 46import constants 47import module_info 48 49from atest_enum import ExitCode 50from test_finders import test_finder_base 51from test_finders import test_info 52from test_runners import test_runner_base as trb 53from test_runners import atest_tf_test_runner as tfr 54 55 56_BAZEL_WORKSPACE_DIR = 'atest_bazel_workspace' 57_SUPPORTED_BAZEL_ARGS = MappingProxyType({ 58 # https://docs.bazel.build/versions/main/command-line-reference.html#flag--runs_per_test 59 constants.ITERATIONS: 60 lambda arg_value: [f'--runs_per_test={str(arg_value)}'], 61 # https://docs.bazel.build/versions/main/command-line-reference.html#flag--test_keep_going 62 constants.RERUN_UNTIL_FAILURE: 63 lambda arg_value: 64 ['--notest_keep_going', f'--runs_per_test={str(arg_value)}'], 65 # https://docs.bazel.build/versions/main/command-line-reference.html#flag--flaky_test_attempts 66 constants.RETRY_ANY_FAILURE: 67 lambda arg_value: [f'--flaky_test_attempts={str(arg_value)}'], 68 constants.BAZEL_ARG: 69 lambda arg_value: [item for sublist in arg_value for item in sublist] 70}) 71 72 73@enum.unique 74class Features(enum.Enum): 75 NULL_FEATURE = ('--null-feature', 'Enables a no-action feature.', True) 76 EXPERIMENTAL_DEVICE_DRIVEN_TEST = ( 77 '--experimental-device-driven-test', 78 'Enables running device-driven tests in Bazel mode.', True) 79 EXPERIMENTAL_BES_PUBLISH = ('--experimental-bes-publish', 80 'Upload test results via BES in Bazel mode.', 81 False) 82 83 def __init__(self, arg_flag, description, affects_workspace): 84 self.arg_flag = arg_flag 85 self.description = description 86 self.affects_workspace = affects_workspace 87 88 89def add_parser_arguments(parser: argparse.ArgumentParser, dest: str): 90 for _, member in Features.__members__.items(): 91 parser.add_argument(member.arg_flag, 92 action='append_const', 93 const=member, 94 dest=dest, 95 help=member.description) 96 97 98def get_bazel_workspace_dir() -> Path: 99 return Path(atest_utils.get_build_out_dir()).joinpath(_BAZEL_WORKSPACE_DIR) 100 101 102def generate_bazel_workspace(mod_info: module_info.ModuleInfo, 103 enabled_features: Set[Features] = None): 104 """Generate or update the Bazel workspace used for running tests.""" 105 106 src_root_path = Path(os.environ.get(constants.ANDROID_BUILD_TOP)) 107 workspace_path = get_bazel_workspace_dir() 108 workspace_generator = WorkspaceGenerator( 109 src_root_path, 110 workspace_path, 111 Path(os.environ.get(constants.ANDROID_PRODUCT_OUT)), 112 Path(os.environ.get(constants.ANDROID_HOST_OUT)), 113 Path(atest_utils.get_build_out_dir()), 114 mod_info, 115 enabled_features, 116 ) 117 workspace_generator.generate() 118 119 120def get_default_build_metadata(): 121 return BuildMetadata(atest_utils.get_manifest_branch(), 122 atest_utils.get_build_target()) 123 124 125class WorkspaceGenerator: 126 """Class for generating a Bazel workspace.""" 127 128 # pylint: disable=too-many-arguments 129 def __init__(self, src_root_path: Path, workspace_out_path: Path, 130 product_out_path: Path, host_out_path: Path, 131 build_out_dir: Path, mod_info: module_info.ModuleInfo, 132 enabled_features: Set[Features] = None): 133 """Initializes the generator. 134 135 Args: 136 src_root_path: Path of the ANDROID_BUILD_TOP. 137 workspace_out_path: Path where the workspace will be output. 138 product_out_path: Path of the ANDROID_PRODUCT_OUT. 139 host_out_path: Path of the ANDROID_HOST_OUT. 140 build_out_dir: Path of OUT_DIR 141 mod_info: ModuleInfo object. 142 enabled_features: Set of enabled features. 143 """ 144 self.enabled_features = enabled_features or set() 145 self.src_root_path = src_root_path 146 self.workspace_out_path = workspace_out_path 147 self.product_out_path = product_out_path 148 self.host_out_path = host_out_path 149 self.build_out_dir = build_out_dir 150 self.mod_info = mod_info 151 self.path_to_package = {} 152 153 def generate(self): 154 """Generate a Bazel workspace. 155 156 If the workspace md5 checksum file doesn't exist or is stale, a new 157 workspace will be generated. Otherwise, the existing workspace will be 158 reused. 159 """ 160 workspace_md5_checksum_file = self.workspace_out_path.joinpath( 161 'workspace_md5_checksum') 162 enabled_features_file = self.workspace_out_path.joinpath( 163 'atest_bazel_mode_enabled_features') 164 enabled_features_file_contents = '\n'.join(sorted( 165 f.name for f in self.enabled_features if f.affects_workspace)) 166 167 if self.workspace_out_path.exists(): 168 # Update the file with the set of the currently enabled features to 169 # make sure that changes are detected in the workspace checksum. 170 enabled_features_file.write_text(enabled_features_file_contents) 171 if atest_utils.check_md5(workspace_md5_checksum_file): 172 return 173 174 # We raise an exception if rmtree fails to avoid leaving stale 175 # files in the workspace that could interfere with execution. 176 shutil.rmtree(self.workspace_out_path) 177 178 atest_utils.colorful_print("Generating Bazel workspace.\n", 179 constants.RED) 180 181 self._add_test_module_targets() 182 183 self.workspace_out_path.mkdir(parents=True) 184 self._generate_artifacts() 185 186 # Note that we write the set of enabled features despite having written 187 # it above since the workspace no longer exists at this point. 188 enabled_features_file.write_text(enabled_features_file_contents) 189 atest_utils.save_md5( 190 [ 191 self.mod_info.mod_info_file_path, 192 enabled_features_file, 193 ], 194 workspace_md5_checksum_file 195 ) 196 197 def _add_test_module_targets(self): 198 seen = set() 199 200 for name, info in self.mod_info.name_to_module_info.items(): 201 # Ignore modules that have a 'host_cross_' prefix since they are 202 # duplicates of existing modules. For example, 203 # 'host_cross_aapt2_tests' is a duplicate of 'aapt2_tests'. We also 204 # ignore modules with a '_32' suffix since these also are redundant 205 # given that modules have both 32 and 64-bit variants built by 206 # default. See b/77288544#comment6 and b/23566667 for more context. 207 if name.endswith("_32") or name.startswith("host_cross_"): 208 continue 209 210 if (Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in 211 self.enabled_features and 212 self.mod_info.is_device_driven_test(info)): 213 self._resolve_dependencies( 214 self._add_test_target( 215 info, 'device', 216 TestTarget.create_device_test_target), seen) 217 218 if self.is_host_unit_test(info): 219 self._resolve_dependencies( 220 self._add_test_target( 221 info, 'host', 222 TestTarget.create_deviceless_test_target), seen) 223 224 def _resolve_dependencies( 225 self, top_level_target: Target, seen: Set[Target]): 226 227 stack = [deque([top_level_target])] 228 229 while stack: 230 top = stack[-1] 231 232 if not top: 233 stack.pop() 234 continue 235 236 target = top.popleft() 237 238 # Note that we're relying on Python's default identity-based hash 239 # and equality methods. This is fine since we actually DO want 240 # reference-equality semantics for Target objects in this context. 241 if target in seen: 242 continue 243 244 seen.add(target) 245 246 next_top = deque() 247 248 for ref in target.dependencies(): 249 info = ref.info or self._get_module_info(ref.name) 250 ref.set(self._add_prebuilt_target(info)) 251 next_top.append(ref.target()) 252 253 stack.append(next_top) 254 255 def _add_test_target(self, info: Dict[str, Any], name_suffix: str, 256 create_fn: Callable) -> Target: 257 package_name = self._get_module_path(info) 258 name = f'{info["module_name"]}_{name_suffix}' 259 260 def create(): 261 return create_fn( 262 name, 263 package_name, 264 info, 265 ) 266 267 return self._add_target(package_name, name, create) 268 269 def _add_prebuilt_target(self, info: Dict[str, Any]) -> Target: 270 package_name = self._get_module_path(info) 271 name = info['module_name'] 272 273 def create(): 274 return SoongPrebuiltTarget.create( 275 self, 276 info, 277 package_name, 278 ) 279 280 return self._add_target(package_name, name, create) 281 282 def _add_target(self, package_path: str, target_name: str, 283 create_fn: Callable) -> Target: 284 285 package = self.path_to_package.get(package_path) 286 287 if not package: 288 package = Package(package_path) 289 self.path_to_package[package_path] = package 290 291 target = package.get_target(target_name) 292 293 if target: 294 return target 295 296 target = create_fn() 297 package.add_target(target) 298 299 return target 300 301 def _get_module_info(self, module_name: str) -> Dict[str, Any]: 302 info = self.mod_info.get_module_info(module_name) 303 304 if not info: 305 raise Exception(f'Could not find module `{module_name}` in' 306 f' module_info file') 307 308 return info 309 310 def _get_module_path(self, info: Dict[str, Any]) -> str: 311 mod_path = info.get(constants.MODULE_PATH) 312 313 if len(mod_path) < 1: 314 module_name = info['module_name'] 315 raise Exception(f'Module `{module_name}` does not have any path') 316 317 if len(mod_path) > 1: 318 module_name = info['module_name'] 319 # We usually have a single path but there are a few exceptions for 320 # modules like libLLVM_android and libclang_android. 321 # TODO(yangbill): Raise an exception for multiple paths once 322 # b/233581382 is resolved. 323 warnings.formatwarning = lambda msg, *args, **kwargs: f'{msg}\n' 324 warnings.warn( 325 f'Module `{module_name}` has more than one path: `{mod_path}`') 326 327 return mod_path[0] 328 329 def is_host_unit_test(self, info: Dict[str, Any]) -> bool: 330 return self.mod_info.is_testable_module( 331 info) and self.mod_info.is_host_unit_test(info) 332 333 def _generate_artifacts(self): 334 """Generate workspace files on disk.""" 335 336 self._create_base_files() 337 self._symlink(src='tools/asuite/atest/bazel/rules', 338 target='bazel/rules') 339 self._symlink(src='tools/asuite/atest/bazel/configs', 340 target='bazel/configs') 341 # Symlink to package with toolchain definitions. 342 self._symlink(src='prebuilts/build-tools', 343 target='prebuilts/build-tools') 344 self._create_constants_file() 345 346 for package in self.path_to_package.values(): 347 package.generate(self.workspace_out_path) 348 349 350 351 def _symlink(self, *, src, target): 352 """Create a symbolic link in workspace pointing to source file/dir. 353 354 Args: 355 src: A string of a relative path to root of Android source tree. 356 This is the source file/dir path for which the symbolic link 357 will be created. 358 target: A string of a relative path to workspace root. This is the 359 target file/dir path where the symbolic link will be created. 360 """ 361 symlink = self.workspace_out_path.joinpath(target) 362 symlink.parent.mkdir(parents=True, exist_ok=True) 363 symlink.symlink_to(self.src_root_path.joinpath(src)) 364 365 def _create_base_files(self): 366 self._symlink(src='tools/asuite/atest/bazel/WORKSPACE', 367 target='WORKSPACE') 368 self._symlink(src='tools/asuite/atest/bazel/bazelrc', 369 target='.bazelrc') 370 self.workspace_out_path.joinpath('BUILD.bazel').touch() 371 372 def _create_constants_file(self): 373 374 def variable_name(target_name): 375 return re.sub(r'[.-]', '_', target_name) + '_label' 376 377 targets = [] 378 seen = set() 379 380 for module_name in TestTarget.DEVICELESS_TEST_PREREQUISITES.union( 381 TestTarget.DEVICE_TEST_PREREQUISITES): 382 info = self.mod_info.get_module_info(module_name) 383 target = self._add_prebuilt_target(info) 384 self._resolve_dependencies(target, seen) 385 targets.append(target) 386 387 with self.workspace_out_path.joinpath( 388 'constants.bzl').open('w') as f: 389 writer = IndentWriter(f) 390 for target in targets: 391 writer.write_line( 392 '%s = "%s"' % 393 (variable_name(target.name()), target.qualified_name()) 394 ) 395 396 397class Package: 398 """Class for generating an entire Package on disk.""" 399 400 def __init__(self, path: str): 401 self.path = path 402 self.imports = defaultdict(set) 403 self.name_to_target = OrderedDict() 404 405 def add_target(self, target): 406 target_name = target.name() 407 408 if target_name in self.name_to_target: 409 raise Exception(f'Cannot add target `{target_name}` which already' 410 f' exists in package `{self.path}`') 411 412 self.name_to_target[target_name] = target 413 414 for i in target.required_imports(): 415 self.imports[i.bzl_package].add(i.symbol) 416 417 def generate(self, workspace_out_path: Path): 418 package_dir = workspace_out_path.joinpath(self.path) 419 package_dir.mkdir(parents=True, exist_ok=True) 420 421 self._create_filesystem_layout(package_dir) 422 self._write_build_file(package_dir) 423 424 def _create_filesystem_layout(self, package_dir: Path): 425 for target in self.name_to_target.values(): 426 target.create_filesystem_layout(package_dir) 427 428 def _write_build_file(self, package_dir: Path): 429 with package_dir.joinpath('BUILD.bazel').open('w') as f: 430 f.write('package(default_visibility = ["//visibility:public"])\n') 431 f.write('\n') 432 433 for bzl_package, symbols in sorted(self.imports.items()): 434 symbols_text = ', '.join('"%s"' % s for s in sorted(symbols)) 435 f.write(f'load("{bzl_package}", {symbols_text})\n') 436 437 for target in self.name_to_target.values(): 438 f.write('\n') 439 target.write_to_build_file(f) 440 441 def get_target(self, target_name: str) -> Target: 442 return self.name_to_target.get(target_name, None) 443 444 445@dataclasses.dataclass(frozen=True) 446class Import: 447 bzl_package: str 448 symbol: str 449 450 451@dataclasses.dataclass(frozen=True) 452class Config: 453 name: str 454 out_path: Path 455 456 457class ModuleRef: 458 459 @staticmethod 460 def for_info(info) -> ModuleRef: 461 return ModuleRef(info=info) 462 463 @staticmethod 464 def for_name(name) -> ModuleRef: 465 return ModuleRef(name=name) 466 467 def __init__(self, info=None, name=None): 468 self.info = info 469 self.name = name 470 self._target = None 471 472 def target(self) -> Target: 473 if not self._target: 474 module_name = self.info['module_name'] 475 raise Exception(f'Target not set for ref `{module_name}`') 476 477 return self._target 478 479 def set(self, target): 480 self._target = target 481 482 483class Target(ABC): 484 """Abstract class for a Bazel target.""" 485 486 @abstractmethod 487 def name(self) -> str: 488 pass 489 490 def package_name(self) -> str: 491 pass 492 493 def qualified_name(self) -> str: 494 return f'//{self.package_name()}:{self.name()}' 495 496 def required_imports(self) -> Set[Import]: 497 return set() 498 499 def supported_configs(self) -> Set[Config]: 500 return set() 501 502 def dependencies(self) -> List[ModuleRef]: 503 return [] 504 505 def write_to_build_file(self, f: IO): 506 pass 507 508 def create_filesystem_layout(self, package_dir: Path): 509 pass 510 511 512class TestTarget(Target): 513 """Class for generating a test target.""" 514 515 DEVICELESS_TEST_PREREQUISITES = frozenset({ 516 'adb', 517 'atest-tradefed', 518 'atest_script_help.sh', 519 'atest_tradefed.sh', 520 'tradefed', 521 'tradefed-test-framework', 522 'bazel-result-reporter' 523 }) 524 525 DEVICE_TEST_PREREQUISITES = frozenset( 526 {'aapt'}).union(DEVICELESS_TEST_PREREQUISITES) 527 528 @staticmethod 529 def create_deviceless_test_target(name: str, package_name: str, 530 info: Dict[str, Any]): 531 return TestTarget(name, package_name, info, 'tradefed_deviceless_test', 532 TestTarget.DEVICELESS_TEST_PREREQUISITES) 533 534 @staticmethod 535 def create_device_test_target(name: str, package_name: str, 536 info: Dict[str, Any]): 537 return TestTarget(name, package_name, info, 'tradefed_device_test', 538 TestTarget.DEVICE_TEST_PREREQUISITES) 539 540 def __init__(self, name: str, package_name: str, info: Dict[str, Any], 541 rule_name: str, prerequisites=frozenset()): 542 self._name = name 543 self._package_name = package_name 544 self._test_module_ref = ModuleRef.for_info(info) 545 self._rule_name = rule_name 546 self._prerequisites = prerequisites 547 548 def name(self) -> str: 549 return self._name 550 551 def package_name(self) -> str: 552 return self._package_name 553 554 def required_imports(self) -> Set[Import]: 555 return { Import('//bazel/rules:tradefed_test.bzl', self._rule_name) } 556 557 def dependencies(self) -> List[ModuleRef]: 558 prerequisite_refs = map(ModuleRef.for_name, self._prerequisites) 559 return [self._test_module_ref] + list(prerequisite_refs) 560 561 def write_to_build_file(self, f: IO): 562 prebuilt_target_name = self._test_module_ref.target().qualified_name() 563 writer = IndentWriter(f) 564 565 writer.write_line(f'{self._rule_name}(') 566 567 with writer.indent(): 568 writer.write_line(f'name = "{self._name}",') 569 writer.write_line(f'test = "{prebuilt_target_name}",') 570 571 writer.write_line(')') 572 573 574class SoongPrebuiltTarget(Target): 575 """Class for generating a Soong prebuilt target on disk.""" 576 577 @staticmethod 578 def create(gen: WorkspaceGenerator, 579 info: Dict[str, Any], 580 package_name: str=''): 581 module_name = info['module_name'] 582 583 configs = [ 584 Config('host', gen.host_out_path), 585 Config('device', gen.product_out_path), 586 ] 587 588 installed_paths = get_module_installed_paths(info, gen.src_root_path) 589 config_files = group_paths_by_config(configs, installed_paths) 590 591 # For test modules, we only create symbolic link to the 'testcases' 592 # directory since the information in module-info is not accurate. 593 # 594 # Note that we use is_tf_testable_module here instead of ModuleInfo 595 # class's is_testable_module method to avoid misadding a shared library 596 # as a test module. 597 # e.g. 598 # 1. test_module A has a shared_lib (or RLIB, DYLIB) of B 599 # 2. We create target B as a result of method _resolve_dependencies for 600 # target A 601 # 3. B matches the conditions of is_testable_module: 602 # a. B has installed path. 603 # b. has_config return True 604 # Note that has_config method also looks for AndroidTest.xml in the 605 # dir of B. If there is a test module in the same dir, B could be 606 # added as a test module. 607 # 4. We create symbolic link to the 'testcases' for non test target B 608 # and cause errors. 609 if is_tf_testable_module(gen.mod_info, info): 610 config_files = {c: [c.out_path.joinpath(f'testcases/{module_name}')] 611 for c in config_files.keys()} 612 613 return SoongPrebuiltTarget( 614 module_name, 615 package_name, 616 config_files, 617 find_runtime_dep_refs(gen.mod_info, info, configs, 618 gen.src_root_path), 619 find_data_dep_refs(gen.mod_info, info, configs, 620 gen.src_root_path) 621 ) 622 623 def __init__(self, name: str, package_name: str, 624 config_files: Dict[Config, List[Path]], 625 runtime_dep_refs: List[ModuleRef], 626 data_dep_refs: List[ModuleRef]): 627 self._name = name 628 self._package_name = package_name 629 self.config_files = config_files 630 self.runtime_dep_refs = runtime_dep_refs 631 self.data_dep_refs = data_dep_refs 632 633 def name(self) -> str: 634 return self._name 635 636 def package_name(self) -> str: 637 return self._package_name 638 639 def required_imports(self) -> Set[Import]: 640 return { 641 Import('//bazel/rules:soong_prebuilt.bzl', self._rule_name()), 642 } 643 644 @functools.lru_cache(maxsize=None) 645 def supported_configs(self) -> Set[Config]: 646 supported_configs = set(self.config_files.keys()) 647 648 if supported_configs: 649 return supported_configs 650 651 # If a target has no installed files, then it supports the same 652 # configurations as its dependencies. This is required because some 653 # build modules are just intermediate targets that don't produce any 654 # output but that still have transitive dependencies. 655 for ref in self.runtime_dep_refs: 656 supported_configs.update(ref.target().supported_configs()) 657 658 return supported_configs 659 660 def dependencies(self) -> List[ModuleRef]: 661 all_deps = set(self.runtime_dep_refs) 662 all_deps.update(self.data_dep_refs) 663 return list(all_deps) 664 665 def write_to_build_file(self, f: IO): 666 writer = IndentWriter(f) 667 668 writer.write_line(f'{self._rule_name()}(') 669 670 with writer.indent(): 671 writer.write_line(f'name = "{self._name}",') 672 writer.write_line(f'module_name = "{self._name}",') 673 self._write_files_attribute(writer) 674 self._write_deps_attribute(writer, 'runtime_deps', 675 self.runtime_dep_refs) 676 self._write_deps_attribute(writer, 'data', self.data_dep_refs) 677 678 writer.write_line(')') 679 680 def create_filesystem_layout(self, package_dir: Path): 681 prebuilts_dir = package_dir.joinpath(self._name) 682 prebuilts_dir.mkdir() 683 684 for config, files in self.config_files.items(): 685 config_prebuilts_dir = prebuilts_dir.joinpath(config.name) 686 config_prebuilts_dir.mkdir() 687 688 for f in files: 689 rel_path = f.relative_to(config.out_path) 690 symlink = config_prebuilts_dir.joinpath(rel_path) 691 symlink.parent.mkdir(parents=True, exist_ok=True) 692 symlink.symlink_to(f) 693 694 def _rule_name(self): 695 return ('soong_prebuilt' if self.config_files 696 else 'soong_uninstalled_prebuilt') 697 698 def _write_files_attribute(self, writer: IndentWriter): 699 if not self.config_files: 700 return 701 702 name = self._name 703 704 writer.write('files = ') 705 write_config_select( 706 writer, 707 self.config_files, 708 lambda c, _: writer.write(f'glob(["{name}/{c.name}/**/*"])'), 709 ) 710 writer.write_line(',') 711 712 def _write_deps_attribute(self, writer, attribute_name, module_refs): 713 config_deps = filter_configs( 714 group_targets_by_config(r.target() for r in module_refs), 715 self.supported_configs() 716 ) 717 718 if not config_deps: 719 return 720 721 for config in self.supported_configs(): 722 config_deps.setdefault(config, []) 723 724 writer.write(f'{attribute_name} = ') 725 write_config_select( 726 writer, 727 config_deps, 728 lambda _, targets: write_target_list(writer, targets), 729 ) 730 writer.write_line(',') 731 732 733def group_paths_by_config( 734 configs: List[Config], paths: List[Path]) -> Dict[Config, List[Path]]: 735 736 config_files = defaultdict(list) 737 738 for f in paths: 739 matching_configs = [ 740 c for c in configs if _is_relative_to(f, c.out_path)] 741 742 if not matching_configs: 743 continue 744 745 # The path can only appear in ANDROID_HOST_OUT for host target or 746 # ANDROID_PRODUCT_OUT, but cannot appear in both. 747 if len(matching_configs) > 1: 748 raise Exception(f'Installed path `{f}` is not in' 749 f' ANDROID_HOST_OUT or ANDROID_PRODUCT_OUT') 750 751 config_files[matching_configs[0]].append(f) 752 753 return config_files 754 755 756def group_targets_by_config( 757 targets: List[Target]) -> Dict[Config, List[Target]]: 758 759 config_to_targets = defaultdict(list) 760 761 for target in targets: 762 for config in target.supported_configs(): 763 config_to_targets[config].append(target) 764 765 return config_to_targets 766 767 768def filter_configs( 769 config_dict: Dict[Config, Any], configs: Set[Config],) -> Dict[Config, Any]: 770 return { k: v for (k, v) in config_dict.items() if k in configs } 771 772 773def _is_relative_to(path1: Path, path2: Path) -> bool: 774 """Return True if the path is relative to another path or False.""" 775 # Note that this implementation is required because Path.is_relative_to only 776 # exists starting with Python 3.9. 777 try: 778 path1.relative_to(path2) 779 return True 780 except ValueError: 781 return False 782 783 784def get_module_installed_paths( 785 info: Dict[str, Any], src_root_path: Path) -> List[Path]: 786 787 # Install paths in module-info are usually relative to the Android 788 # source root ${ANDROID_BUILD_TOP}. When the output directory is 789 # customized by the user however, the install paths are absolute. 790 def resolve(install_path_string): 791 install_path = Path(install_path_string) 792 if not install_path.expanduser().is_absolute(): 793 return src_root_path.joinpath(install_path) 794 return install_path 795 796 return map(resolve, info.get(constants.MODULE_INSTALLED)) 797 798 799def find_runtime_dep_refs( 800 mod_info: module_info.ModuleInfo, 801 info: module_info.Module, 802 configs: List[Config], 803 src_root_path: Path, 804) -> List[ModuleRef]: 805 """Return module references for runtime dependencies.""" 806 807 # We don't use the `dependencies` module-info field for shared libraries 808 # since it's ambiguous and could generate more targets and pull in more 809 # dependencies than necessary. In particular, libraries that support both 810 # static and dynamic linking could end up becoming runtime dependencies 811 # even though the build specifies static linking. For example, if a target 812 # 'T' is statically linked to 'U' which supports both variants, the latter 813 # still appears as a dependency. Since we can't tell, this would result in 814 # the shared library variant of 'U' being added on the library path. 815 libs = set() 816 libs.update(info.get(constants.MODULE_SHARED_LIBS, [])) 817 libs.update(info.get(constants.MODULE_RUNTIME_DEPS, [])) 818 runtime_dep_refs = _find_module_refs(mod_info, configs, src_root_path, libs) 819 820 runtime_library_class = {'RLIB_LIBRARIES', 'DYLIB_LIBRARIES'} 821 # We collect rlibs even though they are technically static libraries since 822 # they could refer to dylibs which are required at runtime. Generating 823 # Bazel targets for these intermediate modules keeps the generator simple 824 # and preserves the shape (isomorphic) of the Soong structure making the 825 # workspace easier to debug. 826 for dep_name in info.get(constants.MODULE_DEPENDENCIES, []): 827 dep_info = mod_info.get_module_info(dep_name) 828 if not dep_info: 829 continue 830 if not runtime_library_class.intersection( 831 dep_info.get(constants.MODULE_CLASS, [])): 832 continue 833 runtime_dep_refs.append(ModuleRef.for_info(dep_info)) 834 835 return runtime_dep_refs 836 837 838def find_data_dep_refs( 839 mod_info: module_info.ModuleInfo, 840 info: module_info.Module, 841 configs: List[Config], 842 src_root_path: Path, 843) -> List[ModuleRef]: 844 """Return module references for data dependencies.""" 845 846 return _find_module_refs(mod_info, 847 configs, 848 src_root_path, 849 info.get(constants.MODULE_DATA_DEPS, [])) 850 851 852def _find_module_refs( 853 mod_info: module_info.ModuleInfo, 854 configs: List[Config], 855 src_root_path: Path, 856 module_names: List[str], 857) -> List[ModuleRef]: 858 """Return module references for modules.""" 859 860 module_refs = [] 861 862 for name in module_names: 863 info = mod_info.get_module_info(name) 864 if not info: 865 continue 866 867 installed_paths = get_module_installed_paths(info, src_root_path) 868 config_files = group_paths_by_config(configs, installed_paths) 869 if not config_files: 870 continue 871 872 module_refs.append(ModuleRef.for_info(info)) 873 874 return module_refs 875 876 877class IndentWriter: 878 879 def __init__(self, f: IO): 880 self._file = f 881 self._indent_level = 0 882 self._indent_string = 4 * ' ' 883 self._indent_next = True 884 885 def write_line(self, text: str=''): 886 if text: 887 self.write(text) 888 889 self._file.write('\n') 890 self._indent_next = True 891 892 def write(self, text): 893 if self._indent_next: 894 self._file.write(self._indent_string * self._indent_level) 895 self._indent_next = False 896 897 self._file.write(text) 898 899 @contextlib.contextmanager 900 def indent(self): 901 self._indent_level += 1 902 yield 903 self._indent_level -= 1 904 905 906def write_config_select( 907 writer: IndentWriter, 908 config_dict: Dict[Config, Any], 909 write_value_fn: Callable, 910): 911 writer.write_line('select({') 912 913 with writer.indent(): 914 for config, value in sorted( 915 config_dict.items(), key=lambda c: c[0].name): 916 917 writer.write(f'"//bazel/rules:{config.name}": ') 918 write_value_fn(config, value) 919 writer.write_line(',') 920 921 writer.write('})') 922 923 924def write_target_list(writer: IndentWriter, targets: List[Target]): 925 writer.write_line('[') 926 927 with writer.indent(): 928 for label in sorted(set(t.qualified_name() for t in targets)): 929 writer.write_line(f'"{label}",') 930 931 writer.write(']') 932 933 934def is_tf_testable_module(mod_info: module_info.ModuleInfo, 935 info: Dict[str, Any]): 936 """Check if the module is a Tradefed runnable test module. 937 938 ModuleInfo.is_testable_module() is from ATest's point of view. It only 939 checks if a module has installed path and has local config files. This 940 way is not reliable since some libraries might match these two conditions 941 and be included mistakenly. Robolectric_utils is an example that matched 942 these two conditions but not testable. This function make sure the module 943 is a TF runnable test module. 944 """ 945 return (mod_info.is_testable_module(info) 946 and info.get(constants.MODULE_COMPATIBILITY_SUITES)) 947 948 949def _decorate_find_method(mod_info, finder_method_func, host, enabled_features): 950 """A finder_method decorator to override TestInfo properties.""" 951 952 def use_bazel_runner(finder_obj, test_id): 953 test_infos = finder_method_func(finder_obj, test_id) 954 if not test_infos: 955 return test_infos 956 for tinfo in test_infos: 957 m_info = mod_info.get_module_info(tinfo.test_name) 958 959 # Only run device-driven tests in Bazel mode when '--host' is not 960 # specified and the feature is enabled. 961 if not host and mod_info.is_device_driven_test(m_info): 962 if Features.EXPERIMENTAL_DEVICE_DRIVEN_TEST in enabled_features: 963 tinfo.test_runner = BazelTestRunner.NAME 964 continue 965 966 if mod_info.is_suite_in_compatibility_suites( 967 'host-unit-tests', m_info): 968 tinfo.test_runner = BazelTestRunner.NAME 969 return test_infos 970 return use_bazel_runner 971 972 973def create_new_finder(mod_info: module_info.ModuleInfo, 974 finder: test_finder_base.TestFinderBase, 975 host: bool, 976 enabled_features: List[Features]=None): 977 """Create new test_finder_base.Finder with decorated find_method. 978 979 Args: 980 mod_info: ModuleInfo object. 981 finder: Test Finder class. 982 host: Whether to run the host variant. 983 enabled_features: List of enabled features. 984 985 Returns: 986 List of ordered find methods. 987 """ 988 return test_finder_base.Finder(finder.test_finder_instance, 989 _decorate_find_method( 990 mod_info, 991 finder.find_method, 992 host, 993 enabled_features or []), 994 finder.finder_info) 995 996 997def default_run_command(args: List[str], cwd: Path) -> str: 998 return subprocess.check_output( 999 args=args, 1000 cwd=cwd, 1001 text=True, 1002 stderr=subprocess.DEVNULL, 1003 ) 1004 1005 1006@dataclasses.dataclass 1007class BuildMetadata: 1008 build_branch: str 1009 build_target: str 1010 1011 1012class BazelTestRunner(trb.TestRunnerBase): 1013 """Bazel Test Runner class.""" 1014 1015 NAME = 'BazelTestRunner' 1016 EXECUTABLE = 'none' 1017 1018 # pylint: disable=redefined-outer-name 1019 # pylint: disable=too-many-arguments 1020 def __init__(self, 1021 results_dir, 1022 mod_info: module_info.ModuleInfo, 1023 extra_args: Dict[str, Any]=None, 1024 test_infos: List[test_info.TestInfo]=None, 1025 src_top: Path=None, 1026 workspace_path: Path=None, 1027 run_command: Callable=default_run_command, 1028 build_metadata: BuildMetadata=None, 1029 env: Dict[str, str]=None, 1030 **kwargs): 1031 super().__init__(results_dir, **kwargs) 1032 self.mod_info = mod_info 1033 self.test_infos = test_infos 1034 self.src_top = src_top or Path(os.environ.get( 1035 constants.ANDROID_BUILD_TOP)) 1036 self.starlark_file = self.src_top.joinpath( 1037 'tools/asuite/atest/bazel/format_as_soong_module_name.cquery') 1038 1039 self.bazel_binary = self.src_top.joinpath( 1040 'prebuilts/bazel/linux-x86_64/bazel') 1041 self.bazel_workspace = workspace_path or get_bazel_workspace_dir() 1042 self.run_command = run_command 1043 self._extra_args = extra_args or {} 1044 self.build_metadata = build_metadata or get_default_build_metadata() 1045 self.env = env or os.environ 1046 1047 # pylint: disable=unused-argument 1048 def run_tests(self, test_infos, extra_args, reporter): 1049 """Run the list of test_infos. 1050 1051 Args: 1052 test_infos: List of TestInfo. 1053 extra_args: Dict of extra args to add to test run. 1054 reporter: An instance of result_report.ResultReporter. 1055 """ 1056 reporter.register_unsupported_runner(self.NAME) 1057 ret_code = ExitCode.SUCCESS 1058 1059 run_cmds = self.generate_run_commands(test_infos, extra_args) 1060 for run_cmd in run_cmds: 1061 subproc = self.run(run_cmd, output_to_stdout=True) 1062 ret_code |= self.wait_for_subprocess(subproc) 1063 return ret_code 1064 1065 def _get_bes_publish_args(self): 1066 args = [] 1067 1068 if not self.env.get("ATEST_BAZEL_BES_PUBLISH_CONFIG"): 1069 return args 1070 1071 config = self.env["ATEST_BAZEL_BES_PUBLISH_CONFIG"] 1072 branch = self.build_metadata.build_branch 1073 target = self.build_metadata.build_target 1074 1075 args.append(f'--config={config}') 1076 args.append(f'--build_metadata=ab_branch={branch}') 1077 args.append(f'--build_metadata=ab_target={target}') 1078 1079 return args 1080 1081 def host_env_check(self): 1082 """Check that host env has everything we need. 1083 1084 We actually can assume the host env is fine because we have the same 1085 requirements that atest has. Update this to check for android env vars 1086 if that changes. 1087 """ 1088 1089 def get_test_runner_build_reqs(self) -> Set[str]: 1090 if not self.test_infos: 1091 return set() 1092 1093 deps_expression = ' + '.join( 1094 sorted(self.test_info_target_label(i) for i in self.test_infos) 1095 ) 1096 1097 query_args = [ 1098 self.bazel_binary, 1099 'cquery', 1100 f'deps(tests({deps_expression}))', 1101 '--output=starlark', 1102 f'--starlark:file={self.starlark_file}', 1103 ] 1104 1105 output = self.run_command(query_args, self.bazel_workspace) 1106 1107 return set(filter(bool, map(str.strip, output.splitlines()))) 1108 1109 def test_info_target_label(self, test: test_info.TestInfo) -> str: 1110 module_name = test.test_name 1111 info = self.mod_info.get_module_info(module_name) 1112 package_name = info.get(constants.MODULE_PATH)[0] 1113 target_suffix = 'host' 1114 1115 if not self._extra_args.get( 1116 constants.HOST, 1117 False) and self.mod_info.is_device_driven_test(info): 1118 target_suffix = 'device' 1119 1120 return f'//{package_name}:{module_name}_{target_suffix}' 1121 1122 # pylint: disable=unused-argument 1123 def generate_run_commands(self, test_infos, extra_args, port=None): 1124 """Generate a list of run commands from TestInfos. 1125 1126 Args: 1127 test_infos: A set of TestInfo instances. 1128 extra_args: A Dict of extra args to append. 1129 port: Optional. An int of the port number to send events to. 1130 1131 Returns: 1132 A list of run commands to run the tests. 1133 """ 1134 startup_options = '' 1135 bazelrc = self.env.get('ATEST_BAZELRC') 1136 1137 if bazelrc: 1138 startup_options = f'--bazelrc={bazelrc}' 1139 1140 target_patterns = ' '.join( 1141 self.test_info_target_label(i) for i in test_infos) 1142 1143 bazel_args = self._parse_extra_args(test_infos, extra_args) 1144 1145 if Features.EXPERIMENTAL_BES_PUBLISH in extra_args.get( 1146 'BAZEL_MODE_FEATURES', []): 1147 bazel_args.extend(self._get_bes_publish_args()) 1148 1149 bazel_args_str = ' '.join(bazel_args) 1150 1151 # Use 'cd' instead of setting the working directory in the subprocess 1152 # call for a working --dry-run command that users can run. 1153 return [ 1154 f'cd {self.bazel_workspace} &&' 1155 f'{self.bazel_binary} {startup_options} ' 1156 f'test {target_patterns} {bazel_args_str}' 1157 ] 1158 1159 def _parse_extra_args(self, test_infos: List[test_info.TestInfo], 1160 extra_args: trb.ARGS) -> trb.ARGS: 1161 args_to_append = [] 1162 # Make a copy of the `extra_args` dict to avoid modifying it for other 1163 # Atest runners. 1164 extra_args_copy = extra_args.copy() 1165 1166 # Map args to their native Bazel counterparts. 1167 for arg in _SUPPORTED_BAZEL_ARGS: 1168 if arg not in extra_args_copy: 1169 continue 1170 args_to_append.extend( 1171 self.map_to_bazel_args(arg, extra_args_copy[arg])) 1172 # Remove the argument since we already mapped it to a Bazel option 1173 # and no longer need it mapped to a Tradefed argument below. 1174 del extra_args_copy[arg] 1175 1176 # TODO(b/215461642): Store the extra_args in the top-level object so 1177 # that we don't have to re-parse the extra args to get BAZEL_ARG again. 1178 tf_args, _ = tfr.extra_args_to_tf_args( 1179 self.mod_info, test_infos, extra_args_copy) 1180 1181 # Add ATest include filter argument to allow testcase filtering. 1182 tf_args.extend(tfr.get_include_filter(test_infos)) 1183 1184 args_to_append.extend([f'--test_arg={i}' for i in tf_args]) 1185 1186 return args_to_append 1187 1188 @staticmethod 1189 def map_to_bazel_args(arg: str, arg_value: Any) -> List[str]: 1190 return _SUPPORTED_BAZEL_ARGS[arg]( 1191 arg_value) if arg in _SUPPORTED_BAZEL_ARGS else [] 1192