blob: 49cdee4ece0e149774e7473954400643b8bcd5fb [file] [log] [blame] [edit]
# Copyright 2019 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Test service.
Handles test related functionality.
"""
import json
import logging
import os
import shutil
from typing import Dict, Iterable, List, NamedTuple, Optional, TYPE_CHECKING
from chromite.cbuildbot import commands
from chromite.lib import autotest_util
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import image_lib
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.utils import code_coverage_util
if TYPE_CHECKING:
from chromite.lib import build_target_lib
from chromite.lib import chroot_lib
from chromite.lib import goma_lib
from chromite.lib import sysroot_lib
from chromite.lib.parser import package_info
class Error(Exception):
"""The module's base error class."""
class NoFilesError(Error):
"""When there are no files to archive."""
class BuildTargetUnitTestResult(object):
"""Result value object."""
def __init__(self, return_code: int,
failed_pkgs: Optional[Iterable['package_info.PackageInfo']]):
"""Init method.
Args:
return_code: The return code from the command execution.
failed_pkgs: List of packages whose tests failed.
"""
self.return_code = return_code
self.failed_pkgs = failed_pkgs or []
@property
def success(self):
return self.return_code == 0 and len(self.failed_pkgs) == 0
def BuildTargetUnitTest(build_target: 'build_target_lib.BuildTarget',
packages: Optional[List[str]] = None,
blocklist: Optional[List[str]] = None,
was_built: bool = True,
code_coverage: bool = False,
testable_packages_optional: bool = False,
filter_only_cros_workon: bool = False
) -> BuildTargetUnitTestResult:
"""Run the ebuild unit tests for the target.
Args:
build_target: The build target.
packages: Packages to be tested. If none, uses all testable packages.
blocklist: Tests to skip.
was_built: Whether packages were built.
code_coverage: Whether to produce code coverage data.
testable_packages_optional: Whether to allow no testable packages to be
found.
filter_only_cros_workon: Whether to filter out non-cros_workon packages from
input package list.
Returns:
BuildTargetUnitTestResult
"""
cros_build_lib.AssertInsideChroot()
# TODO(saklein) Refactor commands.RunUnitTests to use this/the API.
# TODO(crbug.com/960805) Move cros_run_unit_tests logic here.
cmd = ['cros_run_unit_tests']
if build_target.is_host():
cmd.extend(['--host'])
else:
cmd.extend(['--board', build_target.name])
if packages:
cmd.extend(['--packages', ' '.join(packages)])
if blocklist:
cmd.extend(['--skip-packages', ' '.join(blocklist)])
if filter_only_cros_workon:
cmd.append('--filter-only-cros-workon')
if testable_packages_optional:
cmd.append('--no-testable-packages-ok')
if not was_built:
cmd.append('--assume-empty-sysroot')
extra_env = {}
if code_coverage:
use_flags = os.environ.get('USE', '').split()
if 'coverage' not in use_flags:
use_flags.append('coverage')
extra_env['USE'] = ' '.join(use_flags)
# Set up the failed package status file.
with osutils.TempDir() as tempdir:
extra_env[constants.CROS_METRICS_DIR_ENVVAR] = tempdir
result = cros_build_lib.run(
cmd,
extra_env=extra_env,
check=False)
failed_pkgs = portage_util.ParseDieHookStatusFile(tempdir)
return BuildTargetUnitTestResult(result.returncode, failed_pkgs)
def BundleHwqualTarball(board: str, version: str, chroot: 'chroot_lib.Chroot',
sysroot: 'sysroot_lib.Sysroot',
result_path: str) -> Optional[str]:
"""Build the hwqual tarball.
Args:
board: The board name.
version: The version string to use for the image.
chroot: Chroot where the tests were run.
sysroot: The sysroot where the tests were run.
result_path: The directory where the archive should be created.
Returns:
The output path or None.
"""
# Create an autotest.tar.bz2 file to pass to archive_hwqual
# archive_basedir is the base directory where the archive commands are run.
# We want the folder containing the board's autotest folder.
archive_basedir = chroot.full_path(sysroot.path,
constants.AUTOTEST_BUILD_PATH)
archive_basedir = os.path.dirname(archive_basedir)
if not os.path.exists(archive_basedir):
logging.warning('%s does not exist, not creating hwqual', archive_basedir)
return None
with chroot.tempdir() as autotest_bundle_dir:
if not autotest_util.AutotestTarballBuilder(archive_basedir,
autotest_bundle_dir):
logging.warning('could not create autotest bundle, not creating hwqual')
return None
image_dir = image_lib.GetLatestImageLink(board)
ssh_private_key = os.path.join(image_dir, constants.TEST_KEY_PRIVATE)
output_tag = 'chromeos-hwqual-%s-%s' % (board, version)
script_dir = os.path.join(constants.SOURCE_ROOT, 'src', 'platform',
'crostestutils')
cmd = [
os.path.join(script_dir, 'archive_hwqual'), '--from',
autotest_bundle_dir, '--to', result_path, '--image_dir', image_dir,
'--ssh_private_key', ssh_private_key, '--output_tag', output_tag
]
cros_build_lib.run(cmd)
artifact_path = os.path.join(result_path, '%s.tar.bz2' % output_tag)
if not os.path.exists(artifact_path):
return None
return artifact_path
def DebugInfoTest(sysroot_path: str) -> bool:
"""Run the debug info tests.
Args:
sysroot_path: The sysroot being tested.
Returns:
True iff all tests passed, False otherwise.
"""
cmd = ['debug_info_test', os.path.join(sysroot_path, 'usr/lib/debug')]
result = cros_build_lib.run(cmd, enter_chroot=True, check=False)
return result.returncode == 0
def ChromiteUnitTest() -> bool:
"""Run chromite unittests.
Returns:
True iff all tests passed, False otherwise.
"""
cmd = [
os.path.join(constants.CHROMITE_DIR, 'run_tests'),
constants.CHROMITE_DIR,
]
result = cros_build_lib.run(cmd, check=False)
return result.returncode == 0
def RulesCrosUnitTest() -> bool:
"""Run rules_cros unittests.
Returns:
True iff all tests passed, False otherwise.
"""
cmd = [
os.path.join(constants.RULES_CROS_PATH, 'run_tests.sh'),
]
result = cros_build_lib.run(cmd, enter_chroot=True, check=False)
return result.returncode == 0
def SimpleChromeWorkflowTest(sysroot_path: str, build_target_name: str,
chrome_root: str,
goma: Optional['goma_lib.Goma']) -> None:
"""Execute SimpleChrome workflow tests
Args:
sysroot_path: The sysroot path for testing Chrome.
build_target_name: Board build target
chrome_root: Path to Chrome source root.
goma: Goma object or None.
"""
board_dir = 'out_%s' % build_target_name
out_board_dir = os.path.join(chrome_root, board_dir, 'Release')
use_goma = goma is not None
extra_args = []
with osutils.TempDir(prefix='chrome-sdk-cache') as tempdir:
sdk_cmd = _InitSimpleChromeSDK(tempdir, build_target_name, sysroot_path,
chrome_root, use_goma)
if goma:
extra_args.extend(['--nostart-goma', '--gomadir', goma.linux_goma_dir])
_BuildChrome(sdk_cmd, chrome_root, out_board_dir, goma)
_TestDeployChrome(sdk_cmd, out_board_dir)
_VMTestChrome(build_target_name, sdk_cmd)
def _InitSimpleChromeSDK(tempdir: str, build_target_name: str,
sysroot_path: str, chrome_root: str,
use_goma: bool) -> commands.ChromeSDK:
"""Create ChromeSDK object for executing 'cros chrome-sdk' commands.
Args:
tempdir: Tempdir for command execution.
build_target_name: Board build target.
sysroot_path: Sysroot for Chrome to use.
chrome_root: Path to Chrome.
use_goma: Whether to use goma.
Returns:
A ChromeSDK object.
"""
extra_args = ['--cwd', chrome_root, '--sdk-path', sysroot_path]
cache_dir = os.path.join(tempdir, 'cache')
sdk_cmd = commands.ChromeSDK(
constants.SOURCE_ROOT,
build_target_name,
chrome_src=chrome_root,
goma=use_goma,
extra_args=extra_args,
cache_dir=cache_dir)
return sdk_cmd
def _VerifySDKEnvironment(out_board_dir: str) -> None:
"""Make sure the SDK environment is set up properly.
Args:
out_board_dir: Output SDK dir for board.
"""
if not os.path.exists(out_board_dir):
raise AssertionError('%s not created!' % out_board_dir)
logging.info('ARGS.GN=\n%s',
osutils.ReadFile(os.path.join(out_board_dir, 'args.gn')))
def _BuildChrome(sdk_cmd: commands.ChromeSDK, chrome_root: str,
out_board_dir: str, goma: Optional['goma_lib.Goma']) -> None:
"""Build Chrome with SimpleChrome environment.
Args:
sdk_cmd: sdk_cmd to run cros chrome-sdk commands.
chrome_root: Path to Chrome.
out_board_dir: Path to board directory.
goma: Goma object or None
"""
# Validate fetching of the SDK and setting everything up.
sdk_cmd.Run(['true'])
sdk_cmd.Run(['gclient', 'runhooks'])
# Generate args.gn and ninja files.
gn_cmd = os.path.join(chrome_root, 'buildtools', 'linux64', 'gn')
gn_gen_cmd = '%s gen "%s" --args="$GN_ARGS"' % (gn_cmd, out_board_dir)
sdk_cmd.Run(['bash', '-c', gn_gen_cmd])
_VerifySDKEnvironment(out_board_dir)
if goma:
# If goma is enabled, start goma compiler_proxy here, and record
# several information just before building Chrome is started.
goma.Start()
extra_env = goma.GetExtraEnv()
ninja_env_path = os.path.join(goma.goma_log_dir, 'ninja_env')
sdk_cmd.Run(['env', '--null'],
run_args={
'extra_env': extra_env,
'stdout': ninja_env_path
})
osutils.WriteFile(os.path.join(goma.goma_log_dir, 'ninja_cwd'), sdk_cmd.cwd)
osutils.WriteFile(
os.path.join(goma.goma_log_dir, 'ninja_command'),
cros_build_lib.CmdToStr(sdk_cmd.GetNinjaCommand()))
else:
extra_env = None
result = None
try:
# Build chromium.
result = sdk_cmd.Ninja(run_args={'extra_env': extra_env})
finally:
# In teardown, if goma is enabled, stop the goma compiler proxy,
# and record/copy some information to log directory, which will be
# uploaded to the goma's server in a later stage.
if goma:
goma.Stop()
ninja_log_path = os.path.join(chrome_root, sdk_cmd.GetNinjaLogPath())
if os.path.exists(ninja_log_path):
shutil.copy2(ninja_log_path, os.path.join(goma.goma_log_dir,
'ninja_log'))
if result:
osutils.WriteFile(
os.path.join(goma.goma_log_dir, 'ninja_exit'),
str(result.returncode))
def _TestDeployChrome(sdk_cmd: commands.ChromeSDK, out_board_dir: str) -> None:
"""Test SDK deployment.
Args:
sdk_cmd: sdk_cmd to run cros chrome-sdk commands.
out_board_dir: Path to board directory.
"""
with osutils.TempDir(prefix='chrome-sdk-stage') as tempdir:
# Use the TOT deploy_chrome.
script_path = os.path.join(constants.SOURCE_ROOT,
constants.CHROMITE_BIN_SUBDIR, 'deploy_chrome')
sdk_cmd.Run([
script_path, '--build-dir', out_board_dir, '--staging-only',
'--staging-dir', tempdir
])
# Verify chrome is deployed.
chromepath = os.path.join(tempdir, 'chrome')
if not os.path.exists(chromepath):
raise AssertionError(
'deploy_chrome did not run successfully! Searched %s' % (chromepath))
def _VMTestChrome(board: str, sdk_cmd: commands.ChromeSDK) -> None:
"""Run cros_run_test.
Args:
board: The name of the board.
sdk_cmd: sdk_cmd to run cros chrome-sdk commands.
"""
image_dir_symlink = image_lib.GetLatestImageLink(board)
image_path = os.path.join(image_dir_symlink, constants.VM_IMAGE_BIN)
# Run VM test for boards where we've built a VM.
if image_path and os.path.exists(image_path):
sdk_cmd.VMTest(image_path)
def BundleCodeCoverageLlvmJson(chroot: 'chroot_lib.Chroot',
sysroot_class: 'sysroot_lib.Sysroot',
output_dir: str) -> Optional[str]:
"""Bundle code coverage llvm json into a tarball for importing into GCE.
Args:
chroot: The chroot class used for these artifacts.
sysroot_class: The sysroot class used for these artifacts.
output_dir: The path to write artifacts to.
Returns:
A string path to the output code_coverage.tar.xz artifact, or None.
"""
try:
base_path = chroot.full_path(sysroot_class.path)
# Gather all LLVM compiler generated coverage data into single coverage.json
coverage_dir = os.path.join(base_path, 'build/coverage_data')
llvm_generated_cov_json = GatherCodeCoverageLlvmJsonFile(coverage_dir)
llvm_generated_cov_json = (
code_coverage_util.GetLLVMCoverageWithFilesExcluded(
llvm_generated_cov_json,
constants.ZERO_COVERAGE_EXCLUDE_FILES_SUFFIXES))
# Generate zero coverage for all src files, excluding those which are
# already present in llvm_generated_cov_json
files_with_cov = code_coverage_util.ExtractFilenames(
llvm_generated_cov_json)
zero_coverage_json = code_coverage_util.GenerateZeroCoverageLlvm(
# TODO(b/227649725): Input path_to_src_directories and language specific
# src_file_extensions and exclude_line_prefixes from GetArtifact API
path_to_src_directories=[
os.path.join(constants.SOURCE_ROOT, 'src/platform/'),
os.path.join(constants.SOURCE_ROOT, 'src/platform2/')
],
src_file_extensions=constants.ZERO_COVERAGE_FILE_EXTENSIONS_TO_PROCESS,
exclude_line_prefixes=constants.ZERO_COVERAGE_EXCLUDE_LINE_PREFIXES,
exclude_files=files_with_cov,
exclude_files_suffixes=constants.ZERO_COVERAGE_EXCLUDE_FILES_SUFFIXES,
src_prefix_path=constants.SOURCE_ROOT,
extensions_to_remove_exclusion_check
=(constants.EXTENSIONS_TO_REMOVE_EXCLUSION_CHECK))
# Merge generated zero coverage data and
# llvm compiler generated coverage data.
merged_coverage_json = code_coverage_util.MergeLLVMCoverageJson(
llvm_generated_cov_json, zero_coverage_json)
with chroot.tempdir() as dest_tmpdir:
osutils.WriteFile(
os.path.join(dest_tmpdir, constants.CODE_COVERAGE_LLVM_FILE_NAME),
json.dumps(merged_coverage_json))
tarball_path = os.path.join(output_dir,
constants.CODE_COVERAGE_LLVM_JSON_SYMBOLS_TAR)
result = cros_build_lib.CreateTarball(tarball_path, dest_tmpdir)
if result.returncode != 0:
logging.error('Error (%d) when creating tarball %s from %s',
result.returncode, tarball_path, dest_tmpdir)
return None
return tarball_path
except Exception as e:
logging.error('BundleCodeCoverageLlvmJson failed %s', e)
return None
class GatherCodeCoverageLlvmJsonFileResult(NamedTuple):
"""Class containing result data of GatherCodeCoverageLlvmJsonFile."""
coverage_json: Dict
def GatherCodeCoverageLlvmJsonFile(path: str):
"""Locate code coverage llvm json files in |path|.
This function locates all the coverage llvm json files and merges them
into one file, in the correct llvm json format.
Args:
path: The input path to walk.
Returns:
Code coverage json llvm format.
"""
joined_file_paths = []
coverage_data = []
if not os.path.exists(path):
# Builder might only build packages that does not have
# unit test setup,therefore there will be no
# coverage_data to gather.
logging.info('The path does not exists %s. Returning empty coverage.',
path)
return code_coverage_util.CreateLlvmCoverageJson(coverage_data)
if not os.path.isdir(path):
raise ValueError('The path is not a directory: ', path)
for root, _, files in os.walk(path):
for f in files:
# Make sure the file contents match the llvm json format.
path_to_file = os.path.join(root, f)
file_data = code_coverage_util.GetLlvmJsonCoverageDataIfValid(
path_to_file)
if file_data is None:
continue
# Copy over data from this file.
joined_file_paths.append(path_to_file)
for datum in file_data['data']:
for file_data in datum['files']:
coverage_data.append(file_data)
return code_coverage_util.CreateLlvmCoverageJson(coverage_data)
def FindAllMetadataFiles(chroot: 'chroot_lib.Chroot',
sysroot: 'sysroot_lib.Sysroot') -> List[str]:
"""Find the full paths to all test metadata paths."""
# Right now there's no use case for this function inside the chroot.
# If it's useful, we could make the chroot param optional to run in the SDK.
cros_build_lib.AssertOutsideChroot()
return [
_FindAutotestMetadataFile(chroot, sysroot),
_FindTastLocalMetadataFile(chroot, sysroot),
_FindTastLocalPrivateMetadataFile(chroot, sysroot),
_FindTastRemoteMetadataFile(chroot),
]
def _FindAutotestMetadataFile(chroot: 'chroot_lib.Chroot',
sysroot: 'sysroot_lib.Sysroot') -> str:
"""Find the full path to the Autotest test metadata file.
This file is installed during the chromeos-base/autotest ebuild.
"""
return chroot.full_path(
sysroot.Path('usr', 'local', 'build', 'autotest', 'autotest_metadata.pb'))
def _FindTastLocalMetadataFile(chroot: 'chroot_lib.Chroot',
sysroot: 'sysroot_lib.Sysroot') -> str:
"""Find the full path to the Tast local test metadata file.
This file is installed during the tast-bundle eclass.
"""
return chroot.full_path(
sysroot.Path('usr', 'share', 'tast', 'metadata', 'local', 'cros.pb'))
def _FindTastLocalPrivateMetadataFile(chroot: 'chroot_lib.Chroot',
sysroot: 'sysroot_lib.Sysroot') -> str:
"""Find the full path to the Tast local private test metadata file.
This file is installed during the tast-bundle eclass.
"""
return chroot.full_path(
sysroot.Path('build', 'share', 'tast', 'metadata', 'local', 'crosint.pb'))
def _FindTastRemoteMetadataFile(chroot: 'chroot_lib.Chroot') -> str:
"""Find the full path to the Tast remote test metadata file.
This file is installed during the tast-bundle eclass.
"""
return chroot.full_path('usr', 'share', 'tast', 'metadata', 'remote',
'cros.pb')