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