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