blob: 95bb2305866aaabc129849adad16d2ddc124a20c [file] [log] [blame]
# Copyright 2017 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module containing various flavours of VM test stages.
These stages all test the ChromeOS image by running it in some sort of VM.
The tests themselves may vary, as may the harness used to manage the VM.
But they all share some common operations around creating the VM image,
archiving results and VM images in case of failure.
"""
import logging
import os
import re
import shutil
from chromite.cbuildbot import commands
from chromite.cbuildbot.stages import generic_stages
from chromite.lib import cgroups
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cts_helper
from chromite.lib import failures_lib
from chromite.lib import osutils
from chromite.lib import path_util
from chromite.lib import timeout_util
from chromite.service import artifacts as artifacts_svc
_GCE_TEST_RESULTS = "gce_test_results_%(attempt)s"
_VM_TEST_RESULTS = "vm_test_results_%(attempt)s"
_ERROR_MSG = """
!!!%(test_name)s failed!!!
Logs are uploaded in the corresponding %(test_results)s. This can be found
by clicking on the artifacts link in the "Report" Stage. Specifically look
for the test_harness/failed for the failing tests. For more
particulars, please refer to which test failed i.e. above see the
individual test that failed -- or if an update failed, check the
corresponding update directory.
"""
# For sorting through VM test results.
_TEST_REPORT_FILENAME = "test_report.log"
_TEST_PASSED = "PASSED"
_TEST_FAILED = "FAILED"
# This is where the external disk is mounted by moblab VM.
_MOBLAB_STATIC_MOUNT_PATH = os.path.join("/", "mnt", "moblab")
# Must be under '.../static' for staging to work.
_MOBLAB_PAYLOAD_CACHE_DIR = os.path.join("static", "prefetched")
_DEVSERVER_STAGE_URL = (
"http://localhost:8080/stage?local_path=%(payload_dir)s"
"&delete_source=True"
"&artifacts=full_payload,stateful,quick_provision,"
"test_suites,control_files,autotest_packages,autotest_server_package"
)
class VMTestStage(
generic_stages.BoardSpecificBuilderStage, generic_stages.ArchivingStageMixin
):
"""Run autotests in a virtual machine."""
option_name = "tests"
config_name = "vm_tests"
category = constants.TEST_INFRA_STAGE
def __init__(
self,
builder_run,
buildstore,
board,
vm_tests=None,
ssh_port=9228,
test_basename=None,
**kwargs,
):
"""Initiailization of the VMTestStage.
Args:
builder_run: BoardRunAttributes object for this stage.
buildstore: BuildStore instance to make DB calls with.
board: The active board for this stage.
vm_tests: vm_tests to run at this stage. If None is specified, use
builder_run.config.vm_tests instead.
ssh_port: ssh port to access the VM. Default: 9228.
test_basename: The basename that the tests are archived to. If None is
specified, use constants.VM_TEST_RESULTS instead.
"""
self._vm_tests = vm_tests
self._ssh_port = ssh_port
self._test_basename = test_basename
self._stage_exception_handler = super()._HandleStageException
super().__init__(builder_run, buildstore, board, **kwargs)
def _PrintFailedTests(self, results_path, test_basename):
"""Print links to failed tests.
Args:
results_path: Path to directory containing the test results.
test_basename: The basename that the tests are archived to.
"""
test_list = ListTests(results_path, show_passed=False)
for test_name, path in test_list:
self.PrintDownloadLink(
os.path.join(test_basename, path), text_to_display=test_name
)
def _NoTestResults(self, path):
"""Returns True if |path| is not a directory or is an empty directory."""
return not os.path.isdir(path) or not os.listdir(path)
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def _ArchiveTestResults(self, test_results_dir, test_basename):
"""Archives test results to Google Storage.
Args:
test_results_dir: Name of the directory containing the test results.
test_basename: The basename to archive the tests.
"""
results_path = GetTestResultsDir(self._build_root, test_results_dir)
# Skip archiving if results_path does not exist or is an empty directory.
if self._NoTestResults(results_path):
return
archived_results_dir = os.path.join(self.archive_path, test_basename)
# Copy relevant files to archvied_results_dir.
ArchiveTestResults(results_path, archived_results_dir)
upload_paths = [os.path.basename(archived_results_dir)]
# Create the compressed tarball to upload.
# TODO: We should revisit whether uploading the tarball is necessary.
test_tarball = commands.BuildAndArchiveTestResultsTarball(
archived_results_dir, self._build_root
)
upload_paths.append(test_tarball)
got_symbols = self.GetParallel(
"breakpad_symbols_generated", pretty_name="breakpad symbols"
)
upload_paths += commands.GenerateStackTraces(
self._build_root,
self._current_board,
test_results_dir,
self.archive_path,
got_symbols,
)
self._Upload(upload_paths)
self._PrintFailedTests(results_path, test_basename)
# Remove the test results directory.
osutils.RmDir(results_path, ignore_missing=True, sudo=True)
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def _ReportResultsToDashboards(self, test_results_dir):
"""Report VMTests results to chromeperf and CTS dashboard.
Args:
test_results_dir: Name of the directory containing the test results.
"""
# TODO(pwang): also upload to sponge and afe/tko so results show up
# consistently on all dashboards like wmatrix and goldeneye.
results_path = GetTestResultsDir(self._build_root, test_results_dir)
# Skip reporting if results_path does not exist or is an empty directory.
if self._NoTestResults(results_path):
logging.info(
"Found no test results. Skipping upload to dashboards."
)
return
for test_name, test_dir in ListTests(results_path):
if cts_helper.isCtsTest(test_name):
self._ReportCtsResults(
test_name, os.path.join(results_path, test_dir)
)
def _ReportCtsResults(self, test_name, test_dir):
"""Report CTS/GTS result to their dashboards.
Args:
test_name: name of the test.
test_dir: path to the test directory.
"""
logging.info("Reporting cts test: %s in %s", test_name, test_dir)
builder = self._run.GetBuilderName()
buildbucket_id = self._run.options.buildbucket_id
buildbucket_id = str(buildbucket_id)
def _uploader(gs_url, file_path, *args, **kwargs):
directory, filename = os.path.split(file_path)
logging.info("Uploading %s to %s", file_path, gs_url)
commands.UploadArchivedFile(
directory, [gs_url], filename, *args, **kwargs
)
cts_helper.uploadFiles(
test_dir,
builder,
buildbucket_id,
buildbucket_id,
test_name,
_uploader,
self._run.options.debug_forced,
update_list=False,
acl=self.acl,
)
@failures_lib.SetFailureType(failures_lib.InfrastructureFailure)
def _ArchiveVMFiles(self, test_results_dir):
vm_files = ArchiveVMFiles(
self._build_root,
os.path.join(test_results_dir, "test_harness"),
self.archive_path,
)
# We use paths relative to |self.archive_path|, for prettier
# formatting on the web page.
self._Upload([os.path.basename(image) for image in vm_files])
def _Upload(self, filenames):
logging.info("Uploading artifacts to Google Storage...")
with self.ArtifactUploader(archive=False, strict=False) as queue:
for filename in filenames:
queue.put([filename])
if filename.endswith(".dmp.txt"):
prefix = "crash: "
elif constants.VM_DISK_PREFIX in os.path.basename(filename):
prefix = "vm_disk: "
elif constants.VM_MEM_PREFIX in os.path.basename(filename):
prefix = "vm_memory: "
else:
prefix = ""
self.PrintDownloadLink(filename, prefix)
def _RunTest(self, test_config, test_results_dir):
"""Run a VM test.
Args:
test_config: Any config_lib.VMTestConfig with test_type in
constants.VALID_VM_TEST_TYPES.
test_results_dir: The base directory to store the results.
"""
test_type = test_config.test_type
if test_type == constants.CROS_VM_TEST_TYPE:
RunCrosVMTest(
self._build_root, self._current_board, self.GetImageDirSymlink()
)
elif test_type == constants.DEV_MODE_TEST_TYPE:
RunDevModeTest(
self._build_root, self._current_board, self.GetImageDirSymlink()
)
else:
image_path = os.path.join(
self.GetImageDirSymlink(), constants.TEST_IMAGE_BIN
)
ssh_private_key = os.path.join(
self.GetImageDirSymlink(), constants.TEST_KEY_PRIVATE
)
if not os.path.exists(ssh_private_key):
# TODO: Disallow usage of default test key completely.
logging.warning(
"Test key was not found in the image directory. "
"Default key will be used."
)
ssh_private_key = None
RunTestSuite(
self._build_root,
self._current_board,
image_path,
os.path.join(test_results_dir, "test_harness"),
test_config=test_config,
allow_chrome_crashes=self._chrome_rev is None,
ssh_private_key=ssh_private_key,
ssh_port=self._ssh_port,
)
def WaitUntilReady(self):
"""Block until Archive completes tarring autotest/.
The attribute 'autotest_tarball_generated' is set by ArchiveStage.
Returns:
Boolean that authorizes running of this stage.
"""
return self.board_runattrs.GetParallel(
"autotest_tarball_generated", timeout=None
)
def PerformStage(self):
# These directories are used later to archive test artifacts.
if not self._run.options.vmtests:
return
test_results_root = commands.CreateTestRoot(self._build_root)
test_basename = _VM_TEST_RESULTS % dict(attempt=self._attempt)
if self._test_basename:
test_basename = self._test_basename
try:
if not self._vm_tests:
self._vm_tests = self._run.config.vm_tests
failed_tests = []
for vm_test in self._vm_tests:
logging.info("Running VM test %s.", vm_test.test_type)
if vm_test.test_type == constants.VM_SUITE_TEST_TYPE:
per_test_results_dir = os.path.join(
test_results_root, vm_test.test_suite
)
else:
per_test_results_dir = os.path.join(
test_results_root, vm_test.test_type
)
try:
with cgroups.SimpleContainChildren("VMTest"):
r = " Reached VMTestStage test run timeout."
with timeout_util.Timeout(
vm_test.timeout, reason_message=r
):
self._RunTest(vm_test, per_test_results_dir)
except Exception:
failed_tests.append(vm_test)
if vm_test.warn_only:
logging.warning(
"Optional test failed. Forgiving the failure."
)
else:
raise
if failed_tests:
# If any of the tests failed but not raise an exception, mark
# the stage as warning.
self._stage_exception_handler = self._HandleExceptionAsWarning
raise failures_lib.TestWarning(
"VMTestStage succeeded, but some optional tests failed."
)
except Exception as e:
if not isinstance(e, failures_lib.TestWarning):
# pylint: disable=logging-not-lazy
logging.error(
_ERROR_MSG
% dict(test_name="VMTests", test_results=test_basename)
)
self._ArchiveVMFiles(test_results_root)
raise
finally:
if self._run.config.vm_test_report_to_dashboards:
self._ReportResultsToDashboards(test_results_root)
self._ArchiveTestResults(test_results_root, test_basename)
def _HandleStageException(self, exc_info):
return self._stage_exception_handler(exc_info)
def ListTests(results_path, show_failed=True, show_passed=True):
"""Returns a list of tests.
Parse the test report logs from autotest to find tests.
Args:
results_path: Path to the directory of test results.
show_failed: Return failed tests.
show_passed: Return passed tests.
Returns:
A lists of (test_name, relative/path/to/tests)
"""
# TODO: we don't have to parse the log to find tests once
# crbug.com/350520 is fixed.
reports = []
for path, _, filenames in os.walk(results_path):
reports.extend(
[
os.path.join(path, x)
for x in filenames
if x == _TEST_REPORT_FILENAME
]
)
tests = []
processed_tests = []
match_pattern = []
if show_failed:
match_pattern.append(_TEST_FAILED)
if show_passed:
match_pattern.append(_TEST_PASSED)
for report in reports:
logging.info("Parsing test report %s", report)
# Format used in the report:
# /path/to/base/dir/test_harness/all/SimpleTestUpdateAndVerify/ \
# 2_autotest_tests/results-01-security_OpenSSLBlacklist [ FAILED ]
# /path/to/base/dir/test_harness/all/SimpleTestUpdateAndVerify/ \
# 2_autotest_tests/results-01-security_OpenSSLBlacklist/ \
# security_OpenBlacklist [ FAILED ]
with open(report) as f:
folder_re = re.compile(r"([\./\w-]*)\s*\[\s*(\S+?)\s*\]")
test_name_re = re.compile(r"results-[\d]+?-([\.\w_]*)")
for line in f:
r = folder_re.search(line)
if r and r.group(2) in match_pattern:
file_path = r.group(1)
match = test_name_re.search(file_path)
if match:
test_name = match.group(1)
else:
# If no match is found (due to format change or other
# reasons), simply use the last component of file_path.
test_name = os.path.basename(file_path)
# A test may have subtests. We don't want to list all subtests.
if test_name not in processed_tests:
base_dirname = os.path.basename(results_path)
# Get the relative path from the test_results directory. Note
# that file_path is a chroot path, while results_path is a
# non-chroot path, so we cannot use os.path.relpath directly.
rel_path = file_path.split(base_dirname)[1].lstrip(
os.path.sep
)
tests.append((test_name, rel_path))
processed_tests.append(test_name)
return tests
def GetTestResultsDir(buildroot, test_results_dir):
"""Returns the test results directory located in chroot.
Args:
buildroot: Root directory where build occurs.
test_results_dir: Path from buildroot/chroot to find test results.
This must a subdir of /tmp.
"""
test_results_dir = test_results_dir.lstrip("/")
return os.path.join(
buildroot, constants.DEFAULT_CHROOT_DIR, test_results_dir
)
def ArchiveTestResults(results_path, archive_dir):
"""Archives the test results to |archive_dir|.
Args:
results_path: Path to test results.
archive_dir: Local directory to archive to.
"""
cros_build_lib.sudo_run(
["chmod", "-R", "a+rw", results_path], print_cmd=False
)
if os.path.exists(archive_dir):
osutils.RmDir(archive_dir)
def _ShouldIgnore(dirname, file_list):
# Note: We exclude VM disk and memory images. Instead, they are
# archived via ArchiveVMFiles. Also skip any symlinks. gsutil
# hangs on broken symlinks.
return [
x
for x in file_list
if x.startswith(constants.VM_DISK_PREFIX)
or x.startswith(constants.VM_MEM_PREFIX)
or os.path.islink(os.path.join(dirname, x))
]
shutil.copytree(
results_path, archive_dir, symlinks=False, ignore=_ShouldIgnore
)
def ArchiveVMFiles(buildroot, test_results_dir, archive_path):
"""Archives the VM memory and disk images into tarballs.
There may be multiple tests (e.g. SimpleTestUpdate and
SimpleTestUpdateAndVerify), and multiple files for each test (one
for the VM disk, and one for the VM memory). We create a separate
tar file for each of these files, so that each can be downloaded
independently.
Args:
buildroot: Build root directory.
test_results_dir: Path from buildroot/chroot to find test results.
This must a subdir of /tmp.
archive_path: Directory the tarballs should be written to.
Returns:
The paths to the tarballs.
"""
images_dir = os.path.join(buildroot, "chroot", test_results_dir.lstrip("/"))
return artifacts_svc.ArchiveFilesFromImageDir(images_dir, archive_path)
def RunCrosVMTest(buildroot, board, image_dir):
"""Runs cros_vm_test script to verify cros commands work."""
image_path = os.path.join(image_dir, constants.TEST_IMAGE_BIN)
script = os.path.join(
buildroot, "chromite", "cli", "cros", "tests", "cros_vm_test"
)
commands.RunBuildScript(
buildroot, [script, "--board", board, "--image_path", image_path]
)
def RunDevModeTest(buildroot, board, image_dir):
"""Runs the dev mode testing script to verify dev-mode scripts work."""
crostestutils = os.path.join(buildroot, "src", "platform", "crostestutils")
image_path = os.path.join(image_dir, constants.TEST_IMAGE_BIN)
test_script = "devmode-test/devinstall_test.py"
cmd = [
os.path.join(crostestutils, test_script),
"--verbose",
board,
image_path,
]
cros_build_lib.run(cmd)
def RunTestSuite(
buildroot,
board,
image_path,
results_dir,
test_config,
allow_chrome_crashes,
ssh_private_key=None,
ssh_port=9228,
):
"""Runs the test harness suite."""
if (
test_config.use_ctest
or test_config.test_type != constants.VM_SUITE_TEST_TYPE
):
_RunTestSuiteUsingCtest(
buildroot,
board,
image_path,
results_dir,
test_config,
allow_chrome_crashes,
ssh_private_key,
ssh_port,
)
else:
_RunTestSuiteUsingChromite(
board,
image_path,
results_dir,
test_config,
allow_chrome_crashes,
ssh_private_key,
ssh_port,
)
# TODO(zamorzaev): absorb this function into RunTestSuite after deprecating
# ctest.
def _RunTestSuiteUsingChromite(
board,
image_path,
results_dir,
test_config,
allow_chrome_crashes,
ssh_private_key=None,
ssh_port=9228,
):
"""Runs the test harness suite using the chromite code path."""
image_dir = os.path.dirname(image_path)
image_path = os.path.join(image_dir, constants.TEST_IMAGE_BIN)
cmd = [
"cros_run_test",
"--debug",
"--board=%s" % board,
"--image-path=%s" % path_util.ToChrootPath(image_path),
"--ssh-port=%s" % ssh_port,
"--autotest=suite:%s" % test_config.test_suite,
"--results-dir=%s" % results_dir,
]
if allow_chrome_crashes:
cmd.append("--test_that-args=--allow-chrome-crashes")
if ssh_private_key is not None:
cmd.append("--private-key=%s" % path_util.ToChrootPath(ssh_private_key))
# Give tests 10 minutes to clean up before shutting down.
result = cros_build_lib.run(
cmd, check=False, kill_timeout=10 * 60, enter_chroot=True
)
if result.returncode:
results_dir_in_chroot = os.path.join(
constants.SOURCE_ROOT,
constants.DEFAULT_CHROOT_DIR,
results_dir.lstrip("/"),
)
if os.path.exists(results_dir_in_chroot):
error = "%s exited with code %d" % (
" ".join(cmd),
result.returncode,
)
with open(
results_dir_in_chroot + "/failed_test_command", "w"
) as failed:
failed.write(error)
raise failures_lib.TestFailure(
"** VMTests failed with code %d **" % result.returncode
)
def _RunTestSuiteUsingCtest(
buildroot,
board,
image_path,
results_dir,
test_config,
allow_chrome_crashes,
ssh_private_key=None,
ssh_port=9228,
):
"""Runs the test harness suite using the ctest code path."""
results_dir_in_chroot = os.path.join(
buildroot, "chroot", results_dir.lstrip("/")
)
osutils.RmDir(results_dir_in_chroot, ignore_missing=True)
test_type = test_config.test_type
cwd = os.path.join(buildroot, "src", "scripts")
dut_type = "gce" if test_type == constants.GCE_SUITE_TEST_TYPE else "vm"
crostestutils = os.path.join(buildroot, "src", "platform", "crostestutils")
cmd = [
os.path.join(
crostestutils, "au_test_harness", "cros_au_test_harness.py"
),
"--board=%s" % board,
"--type=%s" % dut_type,
"--no_graphics",
"--verbose",
"--target_image=%s" % image_path,
"--test_results_root=%s" % results_dir_in_chroot,
]
if test_type not in constants.VALID_VM_TEST_TYPES:
raise AssertionError("Unrecognized test type %r" % test_type)
if test_type in [
constants.VM_SUITE_TEST_TYPE,
constants.GCE_SUITE_TEST_TYPE,
]:
cmd.append("--ssh_port=%s" % ssh_port)
cmd.append("--verify_suite_name=%s" % test_config.test_suite)
cmd.append("--test_prefix=SimpleTestVerify")
if allow_chrome_crashes:
cmd.append("--allow_chrome_crashes")
if ssh_private_key is not None:
cmd.append("--ssh_private_key=%s" % ssh_private_key)
# Give tests 10 minutes to clean up before shutting down.
result = cros_build_lib.run(cmd, cwd=cwd, check=False, kill_timeout=10 * 60)
if result.returncode:
if os.path.exists(results_dir_in_chroot):
error = "%s exited with code %d" % (
" ".join(cmd),
result.returncode,
)
with open(
results_dir_in_chroot + "/failed_test_command", "w"
) as failed:
failed.write(error)
raise failures_lib.TestFailure(
"** VMTests failed with code %d **" % result.returncode
)