blob: b5b0f98ad1d257e235cd3810dd5aeae3caff77d7 [file] [log] [blame]
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# Copyright (c) 2011 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.
"""This module runs a suite of Auto Update tests.
The tests can be run on either a virtual machine or actual device depending
on parameters given. Specific tests can be run by invoking --test_prefix.
Verbose is useful for many of the tests if you want to see individual commands
being run during the update process.
"""
from __future__ import print_function
import argparse
import functools
import os
import pickle
import sys
import tempfile
import unittest
import errno
import constants
sys.path.append(constants.CROSUTILS_LIB_DIR)
sys.path.append(constants.CROS_PLATFORM_ROOT)
sys.path.append(constants.SOURCE_ROOT)
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import dev_server_wrapper
from chromite.lib import parallel
from chromite.lib import sudo
from chromite.lib import timeout_util
from crostestutils.au_test_harness import au_test
from crostestutils.au_test_harness import au_worker
from crostestutils.lib import test_helper
# File location for update cache in given folder.
CACHE_FILE = 'update.cache'
class _LessBacktracingTestResult(unittest._TextTestResult):
"""TestResult class that suppresses stacks for AssertionError."""
# pylint: disable=W0212
def addFailure(self, test, err):
"""Overrides unittest.TestCase.addFailure to suppress stack traces."""
exc_type = err[0]
if exc_type is AssertionError: # There's already plenty of debug output.
self.failures.append((test, ''))
else:
super(_LessBacktracingTestResult, self).addFailure(test, err)
class _LessBacktracingTestRunner(unittest.TextTestRunner):
"""TestRunner class that suppresses stacks for AssertionError.
This class also prints an error message and exits whenever a test fails,
and further throws a TimeoutException if a test takes longer than
MAX_TIMEOUT_SECONDS.
"""
def _makeResult(self):
return _LessBacktracingTestResult(self.stream,
self.descriptions,
self.verbosity)
def run(self, *args, **kwargs):
"""Run the requested test suite.
If the test suite fails, raise a BackgroundFailure.
"""
with timeout_util.Timeout(constants.MAX_TIMEOUT_SECONDS):
test_result = super(_LessBacktracingTestRunner, self).run(*args, **kwargs)
if test_result is None or not test_result.wasSuccessful():
msg = 'Test harness failed. See logs for details.'
raise parallel.BackgroundFailure(msg)
def _ReadUpdateCache(dut_type, target_image):
"""Reads update cache from generate_test_payloads call."""
# TODO(wonderfly): Figure out how to use update cache for GCE images.
if dut_type == 'gce':
return None
path_to_dump = os.path.dirname(target_image)
cache_file = os.path.join(path_to_dump, CACHE_FILE)
if os.path.exists(cache_file):
logging.info('Loading update cache from ' + cache_file)
with open(cache_file) as file_handle:
return pickle.load(file_handle)
return None
def _PrepareTestSuite(opts):
"""Returns a prepared test suite given by the opts and test class."""
au_test.AUTest.ProcessOptions(opts)
test_loader = unittest.TestLoader()
test_loader.testMethodPrefix = opts.test_prefix
return test_loader.loadTestsFromTestCase(au_test.AUTest)
def _RunTestsInParallel(opts):
"""Runs the tests given by the opts in parallel."""
test_suite = _PrepareTestSuite(opts)
steps = []
for test in test_suite:
test_name = test.id()
test_case = unittest.TestLoader().loadTestsFromName(test_name)
steps.append(functools.partial(_LessBacktracingTestRunner().run, test_case))
logging.info('Running tests in test suite in parallel.')
try:
parallel.RunParallelSteps(steps, max_parallel=opts.jobs)
except parallel.BackgroundFailure as ex:
cros_build_lib.Die(ex)
def CheckOpts(parser, opts):
"""Assert given opts are valid.
Args:
parser: Parser used to parse opts.
opts: Parsed opts.
"""
if not opts.type in ['real', 'vm', 'gce']:
parser.error('Failed to specify valid test type.')
def _IsValidImage(image):
"""Asserts that |image_path| is a valid image file for |opts.type|."""
return (image is not None) and os.path.isfile(image)
if not _IsValidImage(opts.target_image):
parser.error('Testing requires a valid target image.\n'
'Given: type=%s, target_image=%s.' %
(opts.type, opts.target_image))
if not opts.base_image:
logging.info('No base image supplied. Using target as base image.')
opts.base_image = opts.target_image
if not _IsValidImage(opts.base_image):
parser.error('Testing requires a valid base image.\n'
'Given: type=%s, base_image=%s.' %
(opts.type, opts.base_image))
if (opts.payload_signing_key and not
os.path.isfile(opts.payload_signing_key)):
parser.error('Testing requires a valid path to the private key.')
if opts.ssh_private_key and not os.path.isfile(opts.ssh_private_key):
parser.error('Testing requires a valid path to the ssh private key.')
if opts.ssh_port and opts.ssh_port < 1024:
parser.error('Testing requires a valid port higher than 1024.')
if opts.ssh_port and not opts.test_prefix:
parser.error('Testing with ssh_port requires test_prefix specified.')
if opts.test_results_root:
if not 'chroot/tmp' in opts.test_results_root:
parser.error('Must specify a test results root inside tmp in a chroot.')
if not os.path.exists(opts.test_results_root):
os.makedirs(opts.test_results_root)
else:
chroot_tmp = os.path.join(constants.SOURCE_ROOT, 'chroot', 'tmp')
opts.test_results_root = tempfile.mkdtemp(
prefix='au_test_harness', dir=chroot_tmp)
def main():
test_helper.SetupCommonLoggingFormat()
parser = argparse.ArgumentParser()
parser.add_argument('-b', '--base_image',
help='path to the base image.')
parser.add_argument('-r', '--board',
help='board for the images.')
parser.add_argument('--no_delta', action='store_false', default=True,
dest='delta',
help='Disable using delta updates.')
parser.add_argument('--no_graphics', action='store_true',
help='Disable graphics for the vm test.')
parser.add_argument('-j', '--jobs',
default=test_helper.CalculateDefaultJobs(), type=int,
help='Number of simultaneous jobs')
parser.add_argument('--payload_signing_key', default=None,
help='Path to the private key used to sign payloads '
'with.')
parser.add_argument('-q', '--quick_test', default=False, action='store_true',
help='Use a basic test to verify image.')
parser.add_argument('-m', '--remote',
help='Remote address for real test.')
parser.add_argument('-t', '--target_image',
help='path to the target image.')
parser.add_argument('--test_results_root', default=None,
help='Root directory to store test results. Should '
'be defined relative to chroot root.')
parser.add_argument('--test_prefix', default='test',
help='Only runs tests with specific prefix i.e. '
'testFullUpdateWipeStateful.')
parser.add_argument('-p', '--type', default='vm',
help='type of test to run: [vm, real, gce]. Default: vm.')
parser.add_argument('--verbose', default=True, action='store_true',
help='Print out rather than capture output as much as '
'possible.')
parser.add_argument('--whitelist_chrome_crashes', default=False,
dest='whitelist_chrome_crashes', action='store_true',
help='Treat Chrome crashes as non-fatal.')
parser.add_argument('--verify_suite_name', default=None,
help='Specify the verify suite to run.')
parser.add_argument('--parallel', default=False, dest='parallel',
action='store_true',
help='Run multiple test stages in parallel (applies only '
'to vm tests). Default: False')
parser.add_argument('--ssh_private_key', default=None,
help='Path to the private key to use to ssh into the '
'image as the root user.')
parser.add_argument('--ssh_port', default=None, type=int,
help='ssh port used to ssh into image. (Should only be'
' used with --test_prefix)')
opts = parser.parse_args()
CheckOpts(parser, opts)
# Generate cache of updates to use during test harness.
update_cache = _ReadUpdateCache(opts.type, opts.target_image)
if not update_cache:
msg = ('No update cache found. Update testing will not work. Run '
' cros_generate_update_payloads if this was not intended.')
logging.info(msg)
# Create download folder for payloads for testing.
download_folder = os.path.join(os.path.realpath(os.path.curdir),
'latest_download')
try:
os.makedirs(download_folder)
except OSError as e:
if e.errno != errno.EEXIST:
raise
with sudo.SudoKeepAlive():
au_worker.AUWorker.SetUpdateCache(update_cache)
my_server = None
try:
# Only start a devserver if we'll need it.
if update_cache:
my_server = dev_server_wrapper.DevServerWrapper(
port=dev_server_wrapper.DEFAULT_PORT,
log_dir=opts.test_results_root)
my_server.Start()
if (opts.type == 'vm' or opts.type == 'gce') and opts.parallel:
_RunTestsInParallel(opts)
else:
# TODO(sosa) - Take in a machine pool for a real test.
# Can't run in parallel with only one remote device.
test_suite = _PrepareTestSuite(opts)
test_result = unittest.TextTestRunner().run(test_suite)
if not test_result.wasSuccessful():
cros_build_lib.Die('Test harness failed.')
finally:
if my_server:
my_server.Stop()
if __name__ == '__main__':
main()