1# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import abc
7
8import common
9from autotest_lib.server.cros import provision_actionables as actionables
10
11
12### Constants for label prefixes
13CROS_VERSION_PREFIX = 'cros-version'
14ANDROID_BUILD_VERSION_PREFIX = 'ab-version'
15FW_RW_VERSION_PREFIX = 'fwrw-version'
16FW_RO_VERSION_PREFIX = 'fwro-version'
17
18# Default number of provisions attempts to try if we believe the devserver is
19# flaky.
20FLAKY_DEVSERVER_ATTEMPTS = 2
21
22
23### Helpers to convert value to label
24def cros_version_to_label(image):
25    """
26    Returns the proper label name for a ChromeOS build of |image|.
27
28    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
29    @returns: A string that is the appropriate label name.
30
31    """
32    return CROS_VERSION_PREFIX + ':' + image
33
34
35def fwro_version_to_label(image):
36    """
37    Returns the proper label name for a RO firmware build of |image|.
38
39    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
40    @returns: A string that is the appropriate label name.
41
42    """
43    return FW_RO_VERSION_PREFIX + ':' + image
44
45
46def fwrw_version_to_label(image):
47    """
48    Returns the proper label name for a RW firmware build of |image|.
49
50    @param image: A string of the form 'lumpy-release/R28-3993.0.0'
51    @returns: A string that is the appropriate label name.
52
53    """
54    return FW_RW_VERSION_PREFIX + ':' + image
55
56
57class _SpecialTaskAction(object):
58    """
59    Base class to give a template for mapping labels to tests.
60    """
61
62    __metaclass__ = abc.ABCMeta
63
64
65    # One cannot do
66    #     @abc.abstractproperty
67    #     _actions = {}
68    # so this is the next best thing
69    @abc.abstractproperty
70    def _actions(self):
71        """A dictionary mapping labels to test names."""
72        pass
73
74
75    @abc.abstractproperty
76    def name(self):
77        """The name of this special task to be used in output."""
78        pass
79
80
81    @classmethod
82    def acts_on(cls, label):
83        """
84        Returns True if the label is a label that we recognize as something we
85        know how to act on, given our _actions.
86
87        @param label: The label as a string.
88        @returns: True if there exists a test to run for this label.
89
90        """
91        return label.split(':')[0] in cls._actions
92
93
94    @classmethod
95    def test_for(cls, label):
96        """
97        Returns the test associated with the given (string) label name.
98
99        @param label: The label for which the action is being requested.
100        @returns: The string name of the test that should be run.
101        @raises KeyError: If the name was not recognized as one we care about.
102
103        """
104        return cls._actions[label]
105
106
107    @classmethod
108    def partition(cls, labels):
109        """
110        Filter a list of labels into two sets: those labels that we know how to
111        act on and those that we don't know how to act on.
112
113        @param labels: A list of strings of labels.
114        @returns: A tuple where the first element is a set of unactionable
115                  labels, and the second element is a set of the actionable
116                  labels.
117        """
118        capabilities = set()
119        configurations = set()
120
121        for label in labels:
122            if cls.acts_on(label):
123                configurations.add(label)
124            else:
125                capabilities.add(label)
126
127        return capabilities, configurations
128
129
130class Verify(_SpecialTaskAction):
131    """
132    Tests to verify that the DUT is in a sane, known good state that we can run
133    tests on.  Failure to verify leads to running Repair.
134    """
135
136    _actions = {
137        'modem_repair': actionables.TestActionable('cellular_StaleModemReboot'),
138        # TODO(crbug.com/404421): set rpm action to power_RPMTest after the RPM
139        # is stable in lab (destiny). The power_RPMTest failure led to reset job
140        # failure and that left dut in Repair Failed. Since the test will fail
141        # anyway due to the destiny lab issue, and test retry will retry the
142        # test in another DUT.
143        # This change temporarily disable the RPM check in reset job.
144        # Another way to do this is to remove rpm dependency from tests' control
145        # file. That will involve changes on multiple control files. This one
146        # line change here is a simple temporary fix.
147        'rpm': actionables.TestActionable('dummy_PassServer'),
148    }
149
150    name = 'verify'
151
152
153class Provision(_SpecialTaskAction):
154    """
155    Provisioning runs to change the configuration of the DUT from one state to
156    another.  It will only be run on verified DUTs.
157    """
158
159    # TODO(milleral): http://crbug.com/249555
160    # Create some way to discover and register provisioning tests so that we
161    # don't need to hand-maintain a list of all of them.
162    _actions = {
163        CROS_VERSION_PREFIX: actionables.TestActionable(
164                'provision_AutoUpdate',
165                extra_kwargs={'disable_sysinfo': False,
166                              'disable_before_test_sysinfo': False,
167                              'disable_before_iteration_sysinfo': True,
168                              'disable_after_test_sysinfo': True,
169                              'disable_after_iteration_sysinfo': True}),
170        FW_RO_VERSION_PREFIX: actionables.TestActionable(
171                'provision_FirmwareUpdate'),
172        FW_RW_VERSION_PREFIX: actionables.TestActionable(
173                'provision_FirmwareUpdate',
174                extra_kwargs={'rw_only': True}),
175        ANDROID_BUILD_VERSION_PREFIX : actionables.TestActionable(
176                'provision_AndroidUpdate'),
177    }
178
179    name = 'provision'
180
181
182class Cleanup(_SpecialTaskAction):
183    """
184    Cleanup runs after a test fails to try and remove artifacts of tests and
185    ensure the DUT will be in a sane state for the next test run.
186    """
187
188    _actions = {
189        'cleanup-reboot': actionables.RebootActionable(),
190    }
191
192    name = 'cleanup'
193
194
195class Repair(_SpecialTaskAction):
196    """
197    Repair runs when one of the other special tasks fails.  It should be able
198    to take a component of the DUT that's in an unknown state and restore it to
199    a good state.
200    """
201
202    _actions = {
203    }
204
205    name = 'repair'
206
207
208
209# TODO(milleral): crbug.com/364273
210# Label doesn't really mean label in this context.  We're putting things into
211# DEPENDENCIES that really aren't DEPENDENCIES, and we should probably stop
212# doing that.
213def is_for_special_action(label):
214    """
215    If any special task handles the label specially, then we're using the label
216    to communicate that we want an action, and not as an actual dependency that
217    the test has.
218
219    @param label: A string label name.
220    @return True if any special task handles this label specially,
221            False if no special task handles this label.
222    """
223    return (Verify.acts_on(label) or
224            Provision.acts_on(label) or
225            Cleanup.acts_on(label) or
226            Repair.acts_on(label))
227
228
229def filter_labels(labels):
230    """
231    Filter a list of labels into two sets: those labels that we know how to
232    change and those that we don't.  For the ones we know how to change, split
233    them apart into the name of configuration type and its value.
234
235    @param labels: A list of strings of labels.
236    @returns: A tuple where the first element is a set of unprovisionable
237              labels, and the second element is a set of the provisionable
238              labels.
239
240    >>> filter_labels(['bluetooth', 'cros-version:lumpy-release/R28-3993.0.0'])
241    (set(['bluetooth']), set(['cros-version:lumpy-release/R28-3993.0.0']))
242
243    """
244    return Provision.partition(labels)
245
246
247def split_labels(labels):
248    """
249    Split a list of labels into a dict mapping name to value.  All labels must
250    be provisionable labels, or else a ValueError
251
252    @param labels: list of strings of label names
253    @returns: A dict of where the key is the configuration name, and the value
254              is the configuration value.
255    @raises: ValueError if a label is not a provisionable label.
256
257    >>> split_labels(['cros-version:lumpy-release/R28-3993.0.0'])
258    {'cros-version': 'lumpy-release/R28-3993.0.0'}
259    >>> split_labels(['bluetooth'])
260    Traceback (most recent call last):
261    ...
262    ValueError: Unprovisionable label bluetooth
263
264    """
265    configurations = dict()
266
267    for label in labels:
268        if Provision.acts_on(label):
269            name, value = label.split(':', 1)
270            configurations[name] = value
271        else:
272            raise ValueError('Unprovisionable label %s' % label)
273
274    return configurations
275
276
277def join(provision_type, provision_value):
278    """
279    Combine the provision type and value into the label name.
280
281    @param provision_type: One of the constants that are the label prefixes.
282    @param provision_value: A string of the value for this provision type.
283    @returns: A string that is the label name for this (type, value) pair.
284
285    >>> join(CROS_VERSION_PREFIX, 'lumpy-release/R27-3773.0.0')
286    'cros-version:lumpy-release/R27-3773.0.0'
287
288    """
289    return '%s:%s' % (provision_type, provision_value)
290
291
292class SpecialTaskActionException(Exception):
293    """
294    Exception raised when a special task fails to successfully run a test that
295    is required.
296
297    This is also a literally meaningless exception.  It's always just discarded.
298    """
299
300
301def run_special_task_actions(job, host, labels, task):
302    """
303    Iterate through all `label`s and run any tests on `host` that `task` has
304    corresponding to the passed in labels.
305
306    Emits status lines for each run test, and INFO lines for each skipped label.
307
308    @param job: A job object from a control file.
309    @param host: The host to run actions on.
310    @param labels: The list of job labels to work on.
311    @param task: An instance of _SpecialTaskAction.
312    @returns: None
313    @raises: SpecialTaskActionException if a test fails.
314
315    """
316    capabilities, configuration = filter_labels(labels)
317
318    for label in capabilities:
319        if task.acts_on(label):
320            action_item = task.test_for(label)
321            success = action_item.execute(job=job, host=host)
322            if not success:
323                raise SpecialTaskActionException()
324        else:
325            job.record('INFO', None, task.name,
326                       "Can't %s label '%s'." % (task.name, label))
327
328    for name, value in split_labels(configuration).items():
329        if task.acts_on(name):
330            action_item = task.test_for(name)
331            success = action_item.execute(job=job, host=host, value=value)
332            if not success:
333                raise SpecialTaskActionException()
334        else:
335            job.record('INFO', None, task.name,
336                       "Can't %s label '%s:%s'." % (task.name, name, value))
337