blob: e119a8672c67a927d59faf9eee064c78bb80d7c7 [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright 2017 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.
"""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.
"""
from __future__ import print_function
import fnmatch
import os
import re
import shutil
from chromite.cbuildbot import commands
from chromite.cbuildbot.stages import generic_stages
from chromite.cli.cros.tests import cros_vm_test
from chromite.lib import cgroups
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import cts_helper
from chromite.lib import failures_lib
from chromite.lib import osutils
from chromite.lib import timeout_util
_VM_TEST_ERROR_MSG = """
!!!VMTests failed!!!
Logs are uploaded in the corresponding %(vm_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.
"""
_GCE_TEST_ERROR_MSG = """
!!!GCETests failed!!!
Logs are uploaded in the corresponding %(gce_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'
class VMTestStage(generic_stages.BoardSpecificBuilderStage,
generic_stages.ArchivingStageMixin):
"""Run autotests in a virtual machine."""
option_name = 'tests'
config_name = 'vm_tests'
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.debug,
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_type, test_results_dir):
"""Run a VM test.
Args:
test_type: Any test in constants.VALID_VM_TEST_TYPES
test_results_dir: The base directory to store the results.
"""
if test_type == constants.CROS_VM_TEST_TYPE:
RunCrosVMTest(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_type=test_type,
whitelist_chrome_crashes=self._chrome_rev is None,
archive_dir=self.bot_archive_root,
ssh_private_key=ssh_private_key,
)
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 = constants.VM_TEST_RESULTS % dict(attempt=self._attempt)
try:
for vm_test in self._run.config.vm_tests:
test_type = vm_test.test_type
logging.info('Running VM test %s.', test_type)
per_test_results_dir = os.path.join(test_results_root, test_type)
with cgroups.SimpleContainChildren('VMTest'):
r = ' Reached VMTestStage test run timeout.'
with timeout_util.Timeout(vm_test.timeout, reason_message=r):
self._RunTest(test_type, per_test_results_dir)
except Exception:
logging.error(_VM_TEST_ERROR_MSG % dict(vm_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)
class ForgivenVMTestStage(VMTestStage, generic_stages.ForgivingBuilderStage):
"""Stage that forgives vm test failures."""
stage_name = "ForgivenVMTest"
def __init__(self, *args, **kwargs):
super(ForgivenVMTestStage, self).__init__(*args, **kwargs)
class GCETestStage(VMTestStage):
"""Run autotests on a GCE VM instance."""
config_name = 'gce_tests'
# TODO: We should revisit whether GCE tests should have their own configs.
TEST_TIMEOUT = 60 * 60
def _RunTest(self, test_type, test_results_dir):
"""Run a GCE test.
Args:
test_type: Any test in constants.VALID_GCE_TEST_TYPES
test_results_dir: The base directory to store the results.
"""
image_path = os.path.join(self.GetImageDirSymlink(),
constants.TEST_IMAGE_GCE_TAR)
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_type=test_type,
whitelist_chrome_crashes=self._chrome_rev is None,
archive_dir=self.bot_archive_root,
ssh_private_key=ssh_private_key,
)
def PerformStage(self):
# These directories are used later to archive test artifacts.
test_results_root = commands.CreateTestRoot(self._build_root)
test_basename = constants.GCE_TEST_RESULTS % dict(attempt=self._attempt)
try:
for gce_test in self._run.config.gce_tests:
test_type = gce_test.test_type
logging.info('Running GCE test %s.', test_type)
per_test_results_dir = os.path.join(test_results_root, test_type)
with cgroups.SimpleContainChildren('GCETest'):
r = ' Reached GCETestStage test run timeout.'
with timeout_util.Timeout(self.TEST_TIMEOUT, reason_message=r):
self._RunTest(gce_test.test_type, per_test_results_dir)
except Exception:
logging.error(_GCE_TEST_ERROR_MSG % dict(gce_test_results=test_basename))
raise
finally:
self._ArchiveTestResults(test_results_root, test_basename)
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.SudoRunCommand(['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('/'))
images = []
for path, _, filenames in os.walk(images_dir):
images.extend([os.path.join(path, filename) for filename in
fnmatch.filter(filenames, constants.VM_DISK_PREFIX + '*')])
images.extend([os.path.join(path, filename) for filename in
fnmatch.filter(filenames, constants.VM_MEM_PREFIX + '*')])
tar_files = []
for image_path in images:
image_rel_path = os.path.relpath(image_path, images_dir)
image_parent_dir = os.path.dirname(image_path)
image_file = os.path.basename(image_path)
tarball_path = os.path.join(archive_path,
"%s.tar" % image_rel_path.replace('/', '_'))
# Note that tar will chdir to |image_parent_dir|, so that |image_file|
# is at the top-level of the tar file.
cros_build_lib.CreateTarball(tarball_path,
image_parent_dir,
compression=cros_build_lib.COMP_BZIP2,
inputs=[image_file])
tar_files.append(tarball_path)
return tar_files
def RunCrosVMTest(board, image_dir):
"""Runs cros_vm_test script to verify cros commands work."""
image_path = os.path.join(image_dir, constants.TEST_IMAGE_BIN)
test = cros_vm_test.CrosVMTest(board, image_path)
test.Run()
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.RunCommand(cmd)
def RunTestSuite(buildroot, board, image_path, results_dir, test_type,
whitelist_chrome_crashes, archive_dir, ssh_private_key=None):
"""Runs the test harness suite."""
results_dir_in_chroot = os.path.join(buildroot, 'chroot',
results_dir.lstrip('/'))
osutils.RmDir(results_dir_in_chroot, ignore_missing=True)
cwd = os.path.join(buildroot, 'src', 'scripts')
dut_type = 'gce' if test_type in constants.VALID_GCE_TEST_TYPES else 'vm'
cmd = ['bin/ctest',
'--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 +
constants.VALID_GCE_TEST_TYPES):
raise AssertionError('Unrecognized test type %r' % test_type)
if test_type == constants.FULL_AU_TEST_TYPE:
cmd.append('--archive_dir=%s' % archive_dir)
else:
if test_type == constants.SMOKE_SUITE_TEST_TYPE:
cmd.append('--only_verify')
cmd.append('--suite=smoke')
elif test_type == constants.VMTEST_INFORMATIONAL_TEST_TYPE:
cmd.append('--only_verify')
cmd.append('--suite=vmtest-informational')
elif test_type == constants.GCE_SMOKE_TEST_TYPE:
cmd.append('--only_verify')
cmd.append('--suite=gce-smoke')
elif test_type == constants.GCE_SANITY_TEST_TYPE:
cmd.append('--only_verify')
cmd.append('--suite=gce-sanity')
elif test_type == constants.TELEMETRY_SUITE_TEST_TYPE:
cmd.append('--only_verify')
cmd.append('--suite=telemetry_unit_server')
else:
cmd.append('--quick_update')
if whitelist_chrome_crashes:
cmd.append('--whitelist_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.RunCommand(cmd, cwd=cwd, error_code_ok=True,
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)