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 7 There are two types of labels, plain ("webcam") or keyval 8 ("pool:suites"). Most of this module's functions work with keyval 9 labels. 10 11 Most users should use LabelsMapping, which provides a dict-like 12 interface for working with keyval labels. 13 14 This module also provides functions for working with cros version 15 strings, which are common keyval label values. 16 """ 17 18 import collections 19 import re 20 21 22 class Key(object): 23 """Enum for keyval label keys.""" 24 CROS_VERSION = 'cros-version' 25 CROS_ANDROID_VERSION = 'cheets-version' 26 ANDROID_BUILD_VERSION = 'ab-version' 27 TESTBED_VERSION = 'testbed-version' 28 FIRMWARE_RW_VERSION = 'fwrw-version' 29 FIRMWARE_RO_VERSION = 'fwro-version' 30 31 32 class 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 52 def __init__(self, str_labels=()): 53 self._plain_labels = [] 54 self._keyval_map = collections.OrderedDict() 55 for str_label in str_labels: 56 self._add_label(str_label) 57 58 @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 63 def _add_label(self, str_label): 64 """Add a label string to the internal map or plain labels list.""" 65 try: 66 keyval_label = parse_keyval_label(str_label) 67 except ValueError: 68 self._plain_labels.append(str_label) 69 else: 70 if keyval_label.key not in self._keyval_map: 71 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 104 KeyvalLabel = collections.namedtuple('KeyvalLabel', 'key, value') 105 106 107 def 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 116 def format_keyval_label(keyval_label): 117 """Format a KeyvalLabel as a string.""" 118 return _KEYVAL_LABEL_SEP.join(keyval_label) 119 120 121 CrosVersion = collections.namedtuple( 122 'CrosVersion', 'group, board, milestone, version, rc') 123 124 125 _CROS_VERSION_REGEX = ( 126 r'^' 127 r'(?P<group>[a-z0-9_-]+)' 128 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'$' 142 ) 143 144 145 def 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) 154 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) 161 162 163 def 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: 169 return '{group}/{milestone}-{version}'.format(**cros_version._asdict()) 170