blob: 1da7c1cafbdd6516ce6dde3c83c78128d5f0ff4f [file] [log] [blame]
# Copyright 2018 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.
"""Definition of a CrOS suite in skylab.
This file is a simplicication of dynamic_suite.suite without any useless
features for skylab suite.
Suite class in this file mainly has 2 features:
1. Integrate parameters from control file & passed in arguments.
2. Find proper child tests for a given suite.
Use case:
See _run_suite() in skylab_suite.run_suite_skylab.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import collections
import logging
from lucifer import autotest
from skylab_suite import swarming_lib
SuiteSpecs = collections.namedtuple(
'SuiteSpecs',
[
'builds',
'suite_name',
'suite_file_name',
'test_source_build',
'suite_args',
])
RetryHandlerSpecs = collections.namedtuple(
'RetryHandlerSpecs',
[
'timeout_mins',
'test_retry',
'max_retries',
'provision_num_required',
])
class NonValidPropertyError(Exception):
"""Raised if a suite's property is not valid."""
class RetryHandler(object):
"""The class for handling retries for a CrOS suite."""
def __init__(self, specs):
self.wait = True
self.timeout_mins = specs.timeout_mins
self.provision_num_required = specs.provision_num_required
self.test_retry = specs.test_retry
self.max_retries = specs.max_retries
self.suite_id = None
self.task_to_test_maps = {}
self._active_child_tasks = []
def handle_results(self, all_tasks):
"""Handle child tasks' results."""
self._active_child_tasks = [t for t in all_tasks if t['task_id'] in
self.task_to_test_maps]
self.retried_tasks = [t for t in all_tasks if self._should_retry(t)]
logging.info('Found %d tests to be retried.', len(self.retried_tasks))
def finished_waiting(self):
"""Check whether the suite should finish its waiting."""
if self.provision_num_required > 0:
successfully_completed_bots = set()
for t in self._active_child_tasks:
if (t['state'] == swarming_lib.TASK_COMPLETED and
(not t['failure'])):
successfully_completed_bots.add(t['bot_id'])
logging.info('Found %d successfully provisioned bots',
len(successfully_completed_bots))
return (len(successfully_completed_bots) >
self.provision_num_required)
finished_tasks = [t for t in self._active_child_tasks if
t['state'] in swarming_lib.TASK_FINISHED_STATUS]
logging.info('%d/%d child tasks finished, %d got retried.',
len(finished_tasks), len(self._active_child_tasks),
len(self.retried_tasks))
return (len(finished_tasks) == len(self._active_child_tasks)
and not self.retried_tasks)
def _should_retry(self, test_result):
"""Check whether a test should be retried.
We will retry a test if:
1. The test-level retry is enabled for this suite.
2. The test fails.
3. The test is currently monitored by the suite, i.e.
it's not a previous retried test.
4. The test has remaining retries based on JOB_RETRIES in
its control file.
5. The suite-level max retries isn't hit.
@param test_result: A json test result from swarming API.
@return True if we should retry the test.
"""
task_id = test_result['task_id']
state = test_result['state']
is_failure = test_result['failure']
return (self.test_retry and
((state == swarming_lib.TASK_COMPLETED and is_failure)
or (state in swarming_lib.TASK_FAILED_STATUS))
and (task_id in self.task_to_test_maps)
and (self.task_to_test_maps[task_id].remain_retries > 0)
and (self.max_retries > 0))
class Suite(object):
"""The class for a CrOS suite."""
def __init__(self, specs):
"""Initialize a suite.
@param specs: A SuiteSpecs object.
"""
self._ds = None
self.control_file = ''
self.tests = {}
self.builds = specs.builds
self.test_source_build = specs.test_source_build
self.suite_name = specs.suite_name
self.suite_file_name = specs.suite_file_name
@property
def ds(self):
"""Getter for private |self._ds| property.
This ensures that once self.ds is called, there's a devserver ready
for it.
"""
if self._ds is None:
raise NonValidPropertyError(
'Property self.ds is None. Please call stage_suite_artifacts() '
'before calling it.')
return self._ds
def prepare(self):
"""Prepare a suite job for execution."""
self._stage_suite_artifacts()
self._parse_suite_args()
self._find_tests()
def _stage_suite_artifacts(self):
"""Stage suite control files and suite-to-tests mapping file.
@param build: The build to stage artifacts.
"""
suite_common = autotest.load('server.cros.dynamic_suite.suite_common')
ds, _ = suite_common.stage_build_artifacts(self.test_source_build)
self._ds = ds
def _parse_suite_args(self):
"""Get the suite args.
The suite args includes:
a. suite args in suite control file.
b. passed-in suite args by user.
"""
suite_common = autotest.load('server.cros.dynamic_suite.suite_common')
self.control_file = suite_common.get_control_file_by_build(
self.test_source_build, self.ds, self.suite_file_name)
def _find_tests(self):
"""Fetch the child tests."""
control_file_getter = autotest.load(
'server.cros.dynamic_suite.control_file_getter')
suite_common = autotest.load('server.cros.dynamic_suite.suite_common')
cf_getter = control_file_getter.DevServerGetter(
self.test_source_build, self.ds)
tests = suite_common.retrieve_for_suite(
cf_getter, self.suite_name)
self.tests = suite_common.filter_tests(tests)
class ProvisionSuite(Suite):
"""The class for a CrOS provision suite."""
def __init__(self, specs):
super(ProvisionSuite, self).__init__(specs)
self._num_required = specs.suite_args['num_required']
# TODO (xixuan): Ideally the dynamic_suite service is designed
# to be decoupled with any lab (RPC) calls. Here to set maximum
# DUT number for provision as 10 first.
self._num_max = 2
def _find_tests(self):
"""Fetch the child tests for provision suite."""
control_file_getter = autotest.load(
'server.cros.dynamic_suite.control_file_getter')
suite_common = autotest.load('server.cros.dynamic_suite.suite_common')
cf_getter = control_file_getter.DevServerGetter(
self.test_source_build, self.ds)
dummy_test = suite_common.retrieve_control_data_for_test(
cf_getter, 'dummy_Pass')
self.tests = [dummy_test] * max(self._num_required, self._num_max)