blob: 8c9f502340f154b4a43708fcff470b6311d77c4f [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
Allen Lie7ec6632016-10-17 14:54:12 -070019import re
20
Allen Lie7ec6632016-10-17 14:54:12 -070021
22class Key(object):
23 """Enum for keyval label keys."""
24 CROS_VERSION = 'cros-version'
Rohit Makasanadf0a3a32017-06-30 13:55:18 -070025 CROS_ANDROID_VERSION = 'cheets-version'
Allen Lie7ec6632016-10-17 14:54:12 -070026 ANDROID_BUILD_VERSION = 'ab-version'
27 TESTBED_VERSION = 'testbed-version'
28 FIRMWARE_RW_VERSION = 'fwrw-version'
29 FIRMWARE_RO_VERSION = 'fwro-version'
30
31
32class LabelsMapping(collections.MutableMapping):
33 """dict-like interface for working with labels.
34
35 The constructor takes an iterable of labels, either plain or keyval.
36 Plain labels are saved internally and ignored except for converting
37 back to string labels. Keyval labels are exposed through a
38 dict-like interface (pop(), keys(), items(), etc. are all
39 supported).
40
41 When multiple keyval labels share the same key, the first one wins.
42
43 The one difference from a dict is that setting a key to None will
44 delete the corresponding keyval label, since it does not make sense
45 for a keyval label to have a None value. Prefer using del or pop()
46 instead of setting a key to None.
47
48 LabelsMapping has one method getlabels() for converting back to
49 string labels.
50 """
51
Prathmesh Prabhu68acc402017-11-09 15:24:15 -080052 def __init__(self, str_labels=()):
Allen Lie7ec6632016-10-17 14:54:12 -070053 self._plain_labels = []
54 self._keyval_map = collections.OrderedDict()
55 for str_label in str_labels:
56 self._add_label(str_label)
57
Allen Liff162b72017-11-13 12:10:33 -080058 @classmethod
59 def from_host(cls, host):
60 """Create instance using a frontend.afe.models.Host object."""
61 return cls(l.name for l in host.labels.all())
62
Allen Lie7ec6632016-10-17 14:54:12 -070063 def _add_label(self, str_label):
Richard Barnette56db2532017-11-29 14:50:35 -080064 """Add a label string to the internal map or plain labels list."""
Allen Lie7ec6632016-10-17 14:54:12 -070065 try:
66 keyval_label = parse_keyval_label(str_label)
67 except ValueError:
68 self._plain_labels.append(str_label)
69 else:
Richard Barnette56db2532017-11-29 14:50:35 -080070 if keyval_label.key not in self._keyval_map:
Allen Lie7ec6632016-10-17 14:54:12 -070071 self._keyval_map[keyval_label.key] = keyval_label.value
72
73 def __getitem__(self, key):
74 return self._keyval_map[key]
75
76 def __setitem__(self, key, val):
77 if val is None:
78 self.pop(key, None)
79 else:
80 self._keyval_map[key] = val
81
82 def __delitem__(self, key):
83 del self._keyval_map[key]
84
85 def __iter__(self):
86 return iter(self._keyval_map)
87
88 def __len__(self):
89 return len(self._keyval_map)
90
91 def getlabels(self):
92 """Return labels as a list of strings."""
93 str_labels = self._plain_labels[:]
94 keyval_labels = (KeyvalLabel(key, value)
95 for key, value in self.iteritems())
96 str_labels.extend(format_keyval_label(label)
97 for label in keyval_labels)
98 return str_labels
99
100
101_KEYVAL_LABEL_SEP = ':'
102
103
104KeyvalLabel = collections.namedtuple('KeyvalLabel', 'key, value')
105
106
107def parse_keyval_label(str_label):
108 """Parse a string as a KeyvalLabel.
109
110 If the argument is not a valid keyval label, ValueError is raised.
111 """
112 key, value = str_label.split(_KEYVAL_LABEL_SEP, 1)
113 return KeyvalLabel(key, value)
114
115
116def format_keyval_label(keyval_label):
117 """Format a KeyvalLabel as a string."""
118 return _KEYVAL_LABEL_SEP.join(keyval_label)
119
120
121CrosVersion = collections.namedtuple(
Dan Shib2751fc2017-05-16 11:05:15 -0700122 'CrosVersion', 'group, board, milestone, version, rc')
Allen Lie7ec6632016-10-17 14:54:12 -0700123
124
125_CROS_VERSION_REGEX = (
Dan Shib2751fc2017-05-16 11:05:15 -0700126 r'^'
Dan Shi02dd0662017-05-23 11:24:32 -0700127 r'(?P<group>[a-z0-9_-]+)'
Dan Shib2751fc2017-05-16 11:05:15 -0700128 r'/'
129 r'(?P<milestone>R[0-9]+)'
130 r'-'
131 r'(?P<version>[0-9.]+)'
132 r'(-(?P<rc>rc[0-9]+))?'
133 r'$'
134)
135
136_CROS_BOARD_FROM_VERSION_REGEX = (
137 r'^'
138 r'(trybot-)?'
139 r'(?P<board>[a-z_-]+)-(release|paladin|pre-cq|test-ap|toolchain)'
140 r'/R.*'
141 r'$'
Allen Lie7ec6632016-10-17 14:54:12 -0700142)
143
144
145def parse_cros_version(version_string):
146 """Parse a string as a CrosVersion.
147
148 If the argument is not a valid cros version, ValueError is raised.
149 Example cros version string: 'lumpy-release/R27-3773.0.0-rc1'
150 """
151 match = re.search(_CROS_VERSION_REGEX, version_string)
152 if match is None:
153 raise ValueError('Invalid cros version string: %r' % version_string)
Dan Shib2751fc2017-05-16 11:05:15 -0700154 parts = match.groupdict()
155 match = re.search(_CROS_BOARD_FROM_VERSION_REGEX, version_string)
156 if match is None:
157 raise ValueError('Invalid cros version string: %r. Failed to parse '
158 'board.' % version_string)
159 parts['board'] = match.group('board')
160 return CrosVersion(**parts)
Allen Lie7ec6632016-10-17 14:54:12 -0700161
162
163def format_cros_version(cros_version):
164 """Format a CrosVersion as a string."""
165 if cros_version.rc is not None:
166 return '{group}/{milestone}-{version}-{rc}'.format(
167 **cros_version._asdict())
168 else:
Rohit Makasana37f5cf02017-06-08 17:21:25 -0700169 return '{group}/{milestone}-{version}'.format(**cros_version._asdict())