275 lines
9.6 KiB
Python
275 lines
9.6 KiB
Python
|
|
# Copyright 2023, The Android Open Source Project
|
||
|
|
#
|
||
|
|
# 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.
|
||
|
|
|
||
|
|
"""
|
||
|
|
Test runner for Roboleaf mode.
|
||
|
|
|
||
|
|
This runner is used to run the tests that have been fully converted to Bazel.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import enum
|
||
|
|
import shlex
|
||
|
|
import os
|
||
|
|
import logging
|
||
|
|
import json
|
||
|
|
import subprocess
|
||
|
|
|
||
|
|
from typing import Any, Dict, List, Set
|
||
|
|
|
||
|
|
from atest import atest_utils
|
||
|
|
from atest import constants
|
||
|
|
from atest import bazel_mode
|
||
|
|
from atest import result_reporter
|
||
|
|
|
||
|
|
from atest.atest_enum import ExitCode
|
||
|
|
from atest.test_finders.test_info import TestInfo
|
||
|
|
from atest.test_runners import test_runner_base
|
||
|
|
from atest.tools.singleton import Singleton
|
||
|
|
|
||
|
|
# Roboleaf maintains allow lists that identify which modules have been
|
||
|
|
# fully converted to bazel. Users of atest can use
|
||
|
|
# --roboleaf-mode=[PROD/STAGING/DEV] to filter by these allow lists.
|
||
|
|
# PROD (default) is the only mode expected to be fully converted and passing.
|
||
|
|
_ALLOW_LIST_PROD_PATH = ('/soong/soong_injection/allowlists/'
|
||
|
|
'mixed_build_prod_allowlist.txt')
|
||
|
|
_ALLOW_LIST_STAGING_PATH = ('/soong/soong_injection/allowlists/'
|
||
|
|
'mixed_build_staging_allowlist.txt')
|
||
|
|
_ROBOLEAF_MODULE_MAP_PATH = ('/soong/soong_injection/metrics/'
|
||
|
|
'converted_modules_path_map.json')
|
||
|
|
_ROBOLEAF_BUILD_CMD = 'build/soong/soong_ui.bash'
|
||
|
|
|
||
|
|
|
||
|
|
@enum.unique
|
||
|
|
class BazelBuildMode(enum.Enum):
|
||
|
|
"Represents different bp2build allow lists to use whening running bazel (b)"
|
||
|
|
OFF = 'off'
|
||
|
|
DEV = 'dev'
|
||
|
|
STAGING = 'staging'
|
||
|
|
PROD = 'prod'
|
||
|
|
|
||
|
|
|
||
|
|
class RoboleafModuleMap(metaclass=Singleton):
|
||
|
|
"""Roboleaf Module Map Singleton class."""
|
||
|
|
|
||
|
|
def __init__(self,
|
||
|
|
module_map_location: str = ''):
|
||
|
|
self._module_map = _generate_map(module_map_location)
|
||
|
|
self.modules_prod = _read_allow_list(_ALLOW_LIST_PROD_PATH)
|
||
|
|
self.modules_staging = _read_allow_list(_ALLOW_LIST_STAGING_PATH)
|
||
|
|
|
||
|
|
def get_map(self) -> Dict[str, str]:
|
||
|
|
"""Return converted module map.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A dictionary of test names that bazel paths for eligible tests,
|
||
|
|
for example { "test_a": "//platform/test_a" }.
|
||
|
|
"""
|
||
|
|
return self._module_map
|
||
|
|
|
||
|
|
def _generate_map(module_map_location: str = '') -> Dict[str, str]:
|
||
|
|
"""Generate converted module map.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
module_map_location: Path of the module_map_location to check.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A dictionary of test names that bazel paths for eligible tests,
|
||
|
|
for example { "test_a": "//platform/test_a" }.
|
||
|
|
"""
|
||
|
|
if not module_map_location:
|
||
|
|
module_map_location = (
|
||
|
|
atest_utils.get_build_out_dir() + _ROBOLEAF_MODULE_MAP_PATH)
|
||
|
|
|
||
|
|
# TODO(b/274161649): It is possible it could be stale on first run.
|
||
|
|
# Invoking m or b test will check/recreate this file. Bug here is
|
||
|
|
# to determine if we can check staleness without a large time penalty.
|
||
|
|
if not os.path.exists(module_map_location):
|
||
|
|
logging.warning('The roboleaf converted modules file: %s was not '
|
||
|
|
'found.', module_map_location)
|
||
|
|
# Attempt to generate converted modules file.
|
||
|
|
try:
|
||
|
|
cmd = _generate_bp2build_command()
|
||
|
|
env_vars = os.environ.copy()
|
||
|
|
logging.info(
|
||
|
|
'Running `bp2build` to generate converted modules file.'
|
||
|
|
'\n%s', ' '.join(cmd))
|
||
|
|
subprocess.check_call(cmd, env=env_vars)
|
||
|
|
except subprocess.CalledProcessError as e:
|
||
|
|
logging.error(e)
|
||
|
|
return {}
|
||
|
|
|
||
|
|
with open(module_map_location, 'r', encoding='utf8') as robo_map:
|
||
|
|
return json.load(robo_map)
|
||
|
|
|
||
|
|
def _read_allow_list(allow_list_location: str = '') -> List[str]:
|
||
|
|
"""Generate a list of modules based on an allow list file.
|
||
|
|
The expected file format is a text file that has a module name on each line.
|
||
|
|
Lines that start with '#' or '//' are considered comments and skipped.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
location: Path of the allow_list file to parse.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A list of module names.
|
||
|
|
"""
|
||
|
|
|
||
|
|
allow_list_location = (
|
||
|
|
atest_utils.get_build_out_dir() + allow_list_location)
|
||
|
|
|
||
|
|
if not os.path.exists(allow_list_location):
|
||
|
|
logging.error('The roboleaf allow list file: %s was not '
|
||
|
|
'found.', allow_list_location)
|
||
|
|
return []
|
||
|
|
with open(allow_list_location, encoding='utf-8') as f:
|
||
|
|
allowed = []
|
||
|
|
for module_name in f.read().splitlines():
|
||
|
|
if module_name.startswith('#') or module_name.startswith('//'):
|
||
|
|
continue
|
||
|
|
allowed.append(module_name)
|
||
|
|
return allowed
|
||
|
|
|
||
|
|
def _generate_bp2build_command() -> List[str]:
|
||
|
|
"""Build command to run bp2build.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A list of commands to run bp2build.
|
||
|
|
"""
|
||
|
|
soong_ui = (
|
||
|
|
f'{os.environ.get(constants.ANDROID_BUILD_TOP, os.getcwd())}/'
|
||
|
|
f'{_ROBOLEAF_BUILD_CMD}')
|
||
|
|
return [soong_ui, '--make-mode', 'bp2build']
|
||
|
|
|
||
|
|
|
||
|
|
class AbortRunException(Exception):
|
||
|
|
"""Roboleaf Abort Run Exception Class."""
|
||
|
|
|
||
|
|
|
||
|
|
class RoboleafTestRunner(test_runner_base.TestRunnerBase):
|
||
|
|
"""Roboleaf Test Runner class."""
|
||
|
|
NAME = 'RoboleafTestRunner'
|
||
|
|
EXECUTABLE = 'b'
|
||
|
|
|
||
|
|
# pylint: disable=unused-argument
|
||
|
|
def generate_run_commands(self,
|
||
|
|
test_infos: Set[Any],
|
||
|
|
extra_args: Dict[str, Any],
|
||
|
|
port: int = None) -> List[str]:
|
||
|
|
"""Generate a list of run commands from TestInfos.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
test_infos: A set of TestInfo instances.
|
||
|
|
extra_args: A Dict of extra args to append.
|
||
|
|
port: Optional. An int of the port number to send events to.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A list of run commands to run the tests.
|
||
|
|
"""
|
||
|
|
target_patterns = ' '.join(
|
||
|
|
self.test_info_target_label(i) for i in test_infos)
|
||
|
|
bazel_args = bazel_mode.parse_args(test_infos, extra_args, None)
|
||
|
|
bazel_args.append('--config=android')
|
||
|
|
bazel_args.append(
|
||
|
|
'--//build/bazel/rules/tradefed:runmode=host_driven_test'
|
||
|
|
)
|
||
|
|
bazel_args_str = ' '.join(shlex.quote(arg) for arg in bazel_args)
|
||
|
|
command = f'{self.EXECUTABLE} test {target_patterns} {bazel_args_str}'
|
||
|
|
results = [command]
|
||
|
|
logging.info("Roboleaf test runner command:\n"
|
||
|
|
"\n".join(results))
|
||
|
|
return results
|
||
|
|
|
||
|
|
def test_info_target_label(self, test: TestInfo) -> str:
|
||
|
|
""" Get bazel path of test
|
||
|
|
|
||
|
|
Args:
|
||
|
|
test: An object of TestInfo.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The bazel path of the test.
|
||
|
|
"""
|
||
|
|
module_map = RoboleafModuleMap().get_map()
|
||
|
|
return f'{module_map[test.test_name]}:{test.test_name}'
|
||
|
|
|
||
|
|
def run_tests(self,
|
||
|
|
test_infos: List[TestInfo],
|
||
|
|
extra_args: Dict[str, Any],
|
||
|
|
reporter: result_reporter.ResultReporter) -> int:
|
||
|
|
"""Run the list of test_infos.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
test_infos: List of TestInfo.
|
||
|
|
extra_args: Dict of extra args to add to test run.
|
||
|
|
reporter: An instance of result_reporter.ResultReporter.
|
||
|
|
"""
|
||
|
|
reporter.register_unsupported_runner(self.NAME)
|
||
|
|
ret_code = ExitCode.SUCCESS
|
||
|
|
try:
|
||
|
|
run_cmds = self.generate_run_commands(test_infos, extra_args)
|
||
|
|
except AbortRunException as e:
|
||
|
|
atest_utils.colorful_print(f'Stop running test(s): {e}',
|
||
|
|
constants.RED)
|
||
|
|
return ExitCode.ERROR
|
||
|
|
for run_cmd in run_cmds:
|
||
|
|
subproc = self.run(run_cmd, output_to_stdout=True)
|
||
|
|
ret_code |= self.wait_for_subprocess(subproc)
|
||
|
|
return ret_code
|
||
|
|
|
||
|
|
def get_test_runner_build_reqs(
|
||
|
|
self,
|
||
|
|
test_infos: List[TestInfo]) -> Set[str]:
|
||
|
|
return set()
|
||
|
|
|
||
|
|
def host_env_check(self) -> None:
|
||
|
|
"""Check that host env has everything we need.
|
||
|
|
|
||
|
|
We actually can assume the host env is fine because we have the same
|
||
|
|
requirements that atest has. Update this to check for android env vars
|
||
|
|
if that changes.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def roboleaf_eligible_tests(
|
||
|
|
self,
|
||
|
|
mode: BazelBuildMode,
|
||
|
|
module_names: List[str]) -> Dict[str, TestInfo]:
|
||
|
|
"""Filter the given module_names to only ones that are currently
|
||
|
|
fully converted with roboleaf (b test) and then filter further by the
|
||
|
|
given allow list specified in BazelBuildMode.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
mode: A BazelBuildMode value to filter by allow list.
|
||
|
|
module_names: A list of module names to check for roboleaf support.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A dictionary keyed by test name and value of Roboleaf TestInfo.
|
||
|
|
"""
|
||
|
|
if not module_names:
|
||
|
|
return {}
|
||
|
|
|
||
|
|
mod_map = RoboleafModuleMap()
|
||
|
|
supported_modules = set(filter(
|
||
|
|
lambda m: m in mod_map.get_map(), module_names))
|
||
|
|
|
||
|
|
|
||
|
|
if mode == BazelBuildMode.PROD:
|
||
|
|
supported_modules = set(filter(
|
||
|
|
lambda m: m in supported_modules, mod_map.modules_prod))
|
||
|
|
elif mode == BazelBuildMode.STAGING:
|
||
|
|
supported_modules = set(filter(
|
||
|
|
lambda m: m in supported_modules, mod_map.modules_staging))
|
||
|
|
|
||
|
|
return {
|
||
|
|
module: TestInfo(module, RoboleafTestRunner.NAME, set())
|
||
|
|
for module in supported_modules
|
||
|
|
}
|