• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2017 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"""This module provides standard functions for working with Autotest labels.
6
7There are two types of labels, plain ("webcam") or keyval
8("pool:suites").  Most of this module's functions work with keyval
9labels.
10
11Most users should use LabelsMapping, which provides a dict-like
12interface for working with keyval labels.
13
14This module also provides functions for working with cros version
15strings, which are common keyval label values.
16"""
17
18import collections
19import logging
20import re
21
22logger = logging.getLogger(__name__)
23
24
25class Key(object):
26    """Enum for keyval label keys."""
27    CROS_VERSION = 'cros-version'
28    CROS_TH_VERSION = 'cros-th-version'
29    ANDROID_BUILD_VERSION = 'ab-version'
30    TESTBED_VERSION = 'testbed-version'
31    FIRMWARE_RW_VERSION = 'fwrw-version'
32    FIRMWARE_RO_VERSION = 'fwro-version'
33
34
35class LabelsMapping(collections.MutableMapping):
36    """dict-like interface for working with labels.
37
38    The constructor takes an iterable of labels, either plain or keyval.
39    Plain labels are saved internally and ignored except for converting
40    back to string labels.  Keyval labels are exposed through a
41    dict-like interface (pop(), keys(), items(), etc. are all
42    supported).
43
44    When multiple keyval labels share the same key, the first one wins.
45
46    The one difference from a dict is that setting a key to None will
47    delete the corresponding keyval label, since it does not make sense
48    for a keyval label to have a None value.  Prefer using del or pop()
49    instead of setting a key to None.
50
51    LabelsMapping has one method getlabels() for converting back to
52    string labels.
53    """
54
55    def __init__(self, str_labels):
56        self._plain_labels = []
57        self._keyval_map = collections.OrderedDict()
58        for str_label in str_labels:
59            self._add_label(str_label)
60
61    def _add_label(self, str_label):
62        """Add a label string to the internal map or plain labels list.
63
64        If there is already a corresponding keyval in the internal map,
65        skip adding the current label.  This is how existing labels code
66        tends to handle it, but duplicate keys should be considered an
67        anomaly.
68        """
69        try:
70            keyval_label = parse_keyval_label(str_label)
71        except ValueError:
72            self._plain_labels.append(str_label)
73        else:
74            if keyval_label.key in self._keyval_map:
75                logger.warning('Duplicate keyval label %r (current map %r)',
76                               str_label, self._keyval_map)
77            else:
78                self._keyval_map[keyval_label.key] = keyval_label.value
79
80    def __getitem__(self, key):
81        return self._keyval_map[key]
82
83    def __setitem__(self, key, val):
84        if val is None:
85            self.pop(key, None)
86        else:
87            self._keyval_map[key] = val
88
89    def __delitem__(self, key):
90        del self._keyval_map[key]
91
92    def __iter__(self):
93        return iter(self._keyval_map)
94
95    def __len__(self):
96        return len(self._keyval_map)
97
98    def getlabels(self):
99        """Return labels as a list of strings."""
100        str_labels = self._plain_labels[:]
101        keyval_labels = (KeyvalLabel(key, value)
102                         for key, value in self.iteritems())
103        str_labels.extend(format_keyval_label(label)
104                          for label in keyval_labels)
105        return str_labels
106
107
108_KEYVAL_LABEL_SEP = ':'
109
110
111KeyvalLabel = collections.namedtuple('KeyvalLabel', 'key, value')
112
113
114def parse_keyval_label(str_label):
115    """Parse a string as a KeyvalLabel.
116
117    If the argument is not a valid keyval label, ValueError is raised.
118    """
119    key, value = str_label.split(_KEYVAL_LABEL_SEP, 1)
120    return KeyvalLabel(key, value)
121
122
123def format_keyval_label(keyval_label):
124    """Format a KeyvalLabel as a string."""
125    return _KEYVAL_LABEL_SEP.join(keyval_label)
126
127
128CrosVersion = collections.namedtuple(
129        'CrosVersion', 'group, board, milestone, version, rc')
130
131
132_CROS_VERSION_REGEX = (
133        r'^'
134        r'(?P<group>[a-z0-9_-]+)'
135        r'/'
136        r'(?P<milestone>R[0-9]+)'
137        r'-'
138        r'(?P<version>[0-9.]+)'
139        r'(-(?P<rc>rc[0-9]+))?'
140        r'$'
141)
142
143_CROS_BOARD_FROM_VERSION_REGEX = (
144        r'^'
145        r'(trybot-)?'
146        r'(?P<board>[a-z_-]+)-(release|paladin|pre-cq|test-ap|toolchain)'
147        r'/R.*'
148        r'$'
149)
150
151
152def parse_cros_version(version_string):
153    """Parse a string as a CrosVersion.
154
155    If the argument is not a valid cros version, ValueError is raised.
156    Example cros version string: 'lumpy-release/R27-3773.0.0-rc1'
157    """
158    match = re.search(_CROS_VERSION_REGEX, version_string)
159    if match is None:
160        raise ValueError('Invalid cros version string: %r' % version_string)
161    parts = match.groupdict()
162    match = re.search(_CROS_BOARD_FROM_VERSION_REGEX, version_string)
163    if match is None:
164        raise ValueError('Invalid cros version string: %r. Failed to parse '
165                         'board.' % version_string)
166    parts['board'] = match.group('board')
167    return CrosVersion(**parts)
168
169
170def format_cros_version(cros_version):
171    """Format a CrosVersion as a string."""
172    if cros_version.rc is not None:
173        return '{group}/{milestone}-{version}-{rc}'.format(
174                **cros_version._asdict())
175    else:
176        return '{group}/{milestone}-{version}'.format(**cros_version._asdict())
177