blob: 8cad931a868978f50cad828d1b5ef577d5250434 [file] [log] [blame]
# 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 re
import shutil
from typing import Iterable, List, NamedTuple, Optional, TYPE_CHECKING
from chromite.cbuildbot import commands
from chromite.lib import autotest_util
from chromite.lib import chroot_lib
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import failures_lib
from chromite.lib import image_lib
from chromite.lib import moblab_vm
from chromite.lib import osutils
from chromite.lib import portage_util
from chromite.lib import sysroot_lib
from chromite.utils import code_coverage_util
if TYPE_CHECKING:
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,
chroot,
packages=None,
blocklist=None,
was_built=True,
code_coverage=False,
testable_packages_optional=False,
filter_only_cros_workon: bool = False):
"""Run the ebuild unit tests for the target.
Args:
build_target (build_target_lib.BuildTarget): The build target.
chroot (chroot_lib.Chroot): The chroot where the tests are running.
packages (list[str]|None): Packages to be tested. If none, uses all testable
packages.
blocklist (list[str]|None): Tests to skip.
was_built (bool): Whether packages were built.
code_coverage (bool): Whether to produce code coverage data.
testable_packages_optional (bool): Whether to allow no testable packages to
be found.
filter_only_cros_workon (bool): Whether to filter out non-cros_workon
packages from input package list.
Returns:
BuildTargetUnitTestResult
"""
# 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', '--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 = chroot.env
if code_coverage:
use_flags = extra_env.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 chroot.tempdir() as tempdir:
extra_env[constants.CROS_METRICS_DIR_ENVVAR] = chroot.chroot_path(tempdir)
result = cros_build_lib.run(
cmd,
enter_chroot=True,
extra_env=extra_env,
chroot_args=chroot.get_enter_args(),
check=False)
failed_pkgs = portage_util.ParseDieHookStatusFile(tempdir)
return BuildTargetUnitTestResult(result.returncode, failed_pkgs)
def BuildTargetUnitTestTarball(chroot, sysroot, result_path):
"""Build the unittest tarball.
Args:
chroot (chroot_lib.Chroot): Chroot where the tests were run.
sysroot (sysroot_lib.Sysroot): The sysroot where the tests were run.
result_path (str): The directory where the archive should be created.
"""
tarball = 'unit_tests.tar'
tarball_path = os.path.join(result_path, tarball)
cwd = chroot.full_path(sysroot.path, constants.UNITTEST_PKG_PATH)
if not os.path.exists(cwd):
return None
result = cros_build_lib.CreateTarball(
tarball_path,
cwd,
chroot=chroot.path,
compression=cros_build_lib.COMP_NONE,
check=False)
return tarball_path if result.returncode == 0 else None
def BundleHwqualTarball(board, version, chroot, sysroot, result_path):
"""Build the hwqual tarball.
Args:
board (str): The board name.
version (str): The version string to use for the image.
chroot (chroot_lib.Chroot): Chroot where the tests were run.
sysroot (sysroot_lib.Sysroot): The sysroot where the tests were run.
result_path (str): 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):
"""Run the debug info tests.
Args:
sysroot_path (str): The sysroot being tested.
Returns:
bool: 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():
"""Run chromite unittests.
Returns:
bool: 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 CreateMoblabVm(workspace_dir, chroot_dir, image_dir):
"""Create the moblab VMs.
Assumes that image_dir is in exactly the state it was after building
a test image and then converting it to a VM image.
Args:
workspace_dir (str): Workspace for the moblab VM.
chroot_dir (str): Directory containing the chroot for the moblab VM.
image_dir (str): Directory containing the VM image.
Returns:
MoblabVm: The resulting VM.
"""
vms = moblab_vm.MoblabVm(workspace_dir, chroot_dir=chroot_dir)
vms.Create(image_dir, dut_image_dir=image_dir, create_vm_images=False)
return vms
def PrepareMoblabVmImageCache(vms, builder, payload_dirs):
"""Preload the given payloads into the moblab VM image cache.
Args:
vms (MoblabVm): The Moblab VM.
builder (str): The builder path, used to name the cache dir.
payload_dirs (list[str]): List of payload directories to load.
Returns:
str: Absolute path to the image cache path.
"""
with vms.MountedMoblabDiskContext() as disk_dir:
image_cache_root = os.path.join(disk_dir, 'static/prefetched')
# If by any chance this path exists, the permission bits are surely
# nonsense, since 'moblab' user doesn't exist on the host system.
osutils.RmDir(image_cache_root, ignore_missing=True, sudo=True)
image_cache_dir = os.path.join(image_cache_root, builder)
osutils.SafeMakedirsNonRoot(image_cache_dir)
for payload_dir in payload_dirs:
osutils.CopyDirContents(payload_dir, image_cache_dir, allow_nonempty=True)
image_cache_rel_dir = image_cache_dir[len(disk_dir):].strip('/')
return os.path.join('/', 'mnt/moblab', image_cache_rel_dir)
def RunMoblabVmTest(chroot, vms, builder, image_cache_dir, results_dir):
"""Run Moblab VM tests.
Args:
chroot (chroot_lib.Chroot): The chroot in which to run tests.
builder (str): The builder path, used to find artifacts on GS.
vms (MoblabVm): The Moblab VMs to test.
image_cache_dir (str): Path to artifacts cache.
results_dir (str): Path to output test results.
"""
with vms.RunVmsContext():
# TODO(evanhernandez): Move many of these arguments to test config.
test_args = [
# moblab in VM takes longer to bring up all upstart services on first
# boot than on physical machines.
'services_init_timeout_m=10',
'target_build="%s"' % builder,
'test_timeout_hint_m=90',
'clear_devserver_cache=False',
'image_storage_server="%s"' % (image_cache_dir.rstrip('/') + '/'),
]
cros_build_lib.run(
[
'test_that',
'--no-quickmerge',
'--results_dir',
results_dir,
'-b',
'moblab-generic-vm',
'localhost:%s' % vms.moblab_ssh_port,
'moblab_DummyServerNoSspSuite',
'--args',
' '.join(test_args),
],
enter_chroot=True,
chroot_args=chroot.get_enter_args(),
)
def SimpleChromeWorkflowTest(sysroot_path, build_target_name, chrome_root,
goma):
"""Execute SimpleChrome workflow tests
Args:
sysroot_path (str): The sysroot path for testing Chrome.
build_target_name (str): Board build target
chrome_root (str): Path to Chrome source root.
goma (goma_util.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, build_target_name, sysroot_path, chrome_root,
use_goma):
"""Create ChromeSDK object for executing 'cros chrome-sdk' commands.
Args:
tempdir (string): Tempdir for command execution.
build_target_name (string): Board build target.
sysroot_path (string): Sysroot for Chrome to use.
chrome_root (string): Path to Chrome.
use_goma (bool): 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):
"""Make sure the SDK environment is set up properly.
Args:
out_board_dir (str): 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, chrome_root, out_board_dir, goma):
"""Build Chrome with SimpleChrome environment.
Args:
sdk_cmd (ChromeSDK object): sdk_cmd to run cros chrome-sdk commands.
chrome_root (string): Path to Chrome.
out_board_dir (string): Path to board directory.
goma (goma_util.Goma): Goma object
"""
# 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, out_board_dir):
"""Test SDK deployment.
Args:
sdk_cmd (ChromeSDK object): sdk_cmd to run cros chrome-sdk commands.
out_board_dir (string): 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, sdk_cmd):
"""Run cros_run_test."""
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 ValidateMoblabVmTest(results_dir):
"""Determine if the VM test passed or not.
Args:
results_dir (str): Path to directory containing test_that results.
Raises:
failures_lib.TestFailure: If dummy_PassServer did not run or failed.
"""
log_file = os.path.join(results_dir, 'debug', 'test_that.INFO')
if not os.path.isfile(log_file):
raise failures_lib.TestFailure('Found no test_that logs at %s' % log_file)
log_file_contents = osutils.ReadFile(log_file)
if not re.match(r'dummy_PassServer\s*\[\s*PASSED\s*]', log_file_contents):
raise failures_lib.TestFailure('Moblab run_suite succeeded, but did '
'not successfully run dummy_PassServer.')
def BundleCodeCoverageLlvmJson(chroot: chroot_lib.Chroot,
sysroot_class: sysroot_lib.Sysroot,
output_dir: 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.
build_target_name: The build target
Returns:
A string path to the output code_coverage.tar.xz artifact, or None.
"""
try:
base_path = chroot.full_path(sysroot_class.path)
with chroot.tempdir() as dest_tmpdir:
coverage_dir = os.path.join(base_path, 'build/coverage_data')
coverage_file = GatherCodeCoverageLlvmJsonFile(
destdir=dest_tmpdir, paths=[coverage_dir])
if coverage_file is None:
logging.warning('No coverage files found in %s.', coverage_dir)
return None
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."""
joined_file_paths: List[str]
def GatherCodeCoverageLlvmJsonFile(
destdir: str,
paths: List[str],
output_file_name='coverage.json') -> GatherCodeCoverageLlvmJsonFileResult:
"""Locate code coverage llvm json files in |paths|.
This function locates all the coverage llvm json files and merges them
into one file, in the correct llvm json format.
Args:
destdir: Where the combined coverage file should be output to.
paths: A list of input paths to walk.
output_file_name: The name of the combined coverage file to output.
Returns:
A CodeCoverageFileTuple containing coverage.json file information or None.
"""
logging.info('GatherCodeCoverageLlvmJsonFile destdir %s paths %s', destdir,
paths)
joined_file_paths = []
coverage_type = None
coverage_version = None
coverage_data = []
for p in paths:
if not os.path.exists(p):
raise NoFilesError('The path did not exist: ', p)
if not os.path.isdir(p):
raise ValueError('The path is not a directory: ', p)
for root, _, files in os.walk(p):
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)
coverage_type = file_data['type']
coverage_version = file_data['version']
for datum in file_data['data']:
for file_data in datum['files']:
coverage_data.append(file_data)
# Make sure some data was processed.
if not coverage_type or coverage_version is None or len(coverage_data) <= 0:
return None
# Write out the file
osutils.WriteFile(
os.path.join(destdir, output_file_name),
json.dumps({
'data': [{
'files': coverage_data
}],
'type': coverage_type,
'version': coverage_version
}))
return GatherCodeCoverageLlvmJsonFileResult(
joined_file_paths=joined_file_paths)
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')