blob: a3ffa2c445b9cb3ebb106cb03bc45caca4b77956 [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'
Rohit Makasanadf0a3a32017-06-30 13:55:18 -070028 CROS_ANDROID_VERSION = 'cheets-version'
Allen Lie7ec6632016-10-17 14:54:12 -070029 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
Prathmesh Prabhu68acc402017-11-09 15:24:15 -080055 def __init__(self, str_labels=()):
Allen Lie7ec6632016-10-17 14:54:12 -070056 self._plain_labels = []
57 self._keyval_map = collections.OrderedDict()
58 for str_label in str_labels:
59 self._add_label(str_label)
60
Allen Liff162b72017-11-13 12:10:33 -080061 @classmethod
62 def from_host(cls, host):
63 """Create instance using a frontend.afe.models.Host object."""
64 return cls(l.name for l in host.labels.all())
65
Allen Lie7ec6632016-10-17 14:54:12 -070066 def _add_label(self, str_label):
67 """Add a label string to the internal map or plain labels list.
68
69 If there is already a corresponding keyval in the internal map,
70 skip adding the current label. This is how existing labels code
71 tends to handle it, but duplicate keys should be considered an
72 anomaly.
73 """
74 try:
75 keyval_label = parse_keyval_label(str_label)
76 except ValueError:
77 self._plain_labels.append(str_label)
78 else:
79 if keyval_label.key in self._keyval_map:
80 logger.warning('Duplicate keyval label %r (current map %r)',
81 str_label, self._keyval_map)
82 else:
83 self._keyval_map[keyval_label.key] = keyval_label.value
84
85 def __getitem__(self, key):
86 return self._keyval_map[key]
87
88 def __setitem__(self, key, val):
89 if val is None:
90 self.pop(key, None)
91 else:
92 self._keyval_map[key] = val
93
94 def __delitem__(self, key):
95 del self._keyval_map[key]
96
97 def __iter__(self):
98 return iter(self._keyval_map)
99
100 def __len__(self):
101 return len(self._keyval_map)
102
103 def getlabels(self):
104 """Return labels as a list of strings."""
105 str_labels = self._plain_labels[:]
106 keyval_labels = (KeyvalLabel(key, value)
107 for key, value in self.iteritems())
108 str_labels.extend(format_keyval_label(label)
109 for label in keyval_labels)
110 return str_labels
111
112
113_KEYVAL_LABEL_SEP = ':'
114
115
116KeyvalLabel = collections.namedtuple('KeyvalLabel', 'key, value')
117
118
119def parse_keyval_label(str_label):
120 """Parse a string as a KeyvalLabel.
121
122 If the argument is not a valid keyval label, ValueError is raised.
123 """
124 key, value = str_label.split(_KEYVAL_LABEL_SEP, 1)
125 return KeyvalLabel(key, value)
126
127
128def format_keyval_label(keyval_label):
129 """Format a KeyvalLabel as a string."""
130 return _KEYVAL_LABEL_SEP.join(keyval_label)
131
132
133CrosVersion = collections.namedtuple(
Dan Shib2751fc2017-05-16 11:05:15 -0700134 'CrosVersion', 'group, board, milestone, version, rc')
Allen Lie7ec6632016-10-17 14:54:12 -0700135
136
137_CROS_VERSION_REGEX = (
Dan Shib2751fc2017-05-16 11:05:15 -0700138 r'^'
Dan Shi02dd0662017-05-23 11:24:32 -0700139 r'(?P<group>[a-z0-9_-]+)'
Dan Shib2751fc2017-05-16 11:05:15 -0700140 r'/'
141 r'(?P<milestone>R[0-9]+)'
142 r'-'
143 r'(?P<version>[0-9.]+)'
144 r'(-(?P<rc>rc[0-9]+))?'
145 r'$'
146)
147
148_CROS_BOARD_FROM_VERSION_REGEX = (
149 r'^'
150 r'(trybot-)?'
151 r'(?P<board>[a-z_-]+)-(release|paladin|pre-cq|test-ap|toolchain)'
152 r'/R.*'
153 r'$'
Allen Lie7ec6632016-10-17 14:54:12 -0700154)
155
156
157def parse_cros_version(version_string):
158 """Parse a string as a CrosVersion.
159
160 If the argument is not a valid cros version, ValueError is raised.
161 Example cros version string: 'lumpy-release/R27-3773.0.0-rc1'
162 """
163 match = re.search(_CROS_VERSION_REGEX, version_string)
164 if match is None:
165 raise ValueError('Invalid cros version string: %r' % version_string)
Dan Shib2751fc2017-05-16 11:05:15 -0700166 parts = match.groupdict()
167 match = re.search(_CROS_BOARD_FROM_VERSION_REGEX, version_string)
168 if match is None:
169 raise ValueError('Invalid cros version string: %r. Failed to parse '
170 'board.' % version_string)
171 parts['board'] = match.group('board')
172 return CrosVersion(**parts)
Allen Lie7ec6632016-10-17 14:54:12 -0700173
174
175def format_cros_version(cros_version):
176 """Format a CrosVersion as a string."""
177 if cros_version.rc is not None:
178 return '{group}/{milestone}-{version}-{rc}'.format(
179 **cros_version._asdict())
180 else:
Rohit Makasana37f5cf02017-06-08 17:21:25 -0700181 return '{group}/{milestone}-{version}'.format(**cros_version._asdict())