1*9c5db199SXin Li# Lint as: python2, python3 2*9c5db199SXin Li# Copyright 2017 The Chromium OS Authors. All rights reserved. 3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be 4*9c5db199SXin Li# found in the LICENSE file. 5*9c5db199SXin Li 6*9c5db199SXin Li"""This module provides standard functions for working with Autotest labels. 7*9c5db199SXin Li 8*9c5db199SXin LiThere are two types of labels, plain ("webcam") or keyval 9*9c5db199SXin Li("pool:suites"). Most of this module's functions work with keyval 10*9c5db199SXin Lilabels. 11*9c5db199SXin Li 12*9c5db199SXin LiMost users should use LabelsMapping, which provides a dict-like 13*9c5db199SXin Liinterface for working with keyval labels. 14*9c5db199SXin Li 15*9c5db199SXin LiThis module also provides functions for working with cros version 16*9c5db199SXin Listrings, which are common keyval label values. 17*9c5db199SXin Li""" 18*9c5db199SXin Li 19*9c5db199SXin Lifrom __future__ import absolute_import 20*9c5db199SXin Lifrom __future__ import division 21*9c5db199SXin Lifrom __future__ import print_function 22*9c5db199SXin Liimport collections 23*9c5db199SXin Liimport re 24*9c5db199SXin Liimport six 25*9c5db199SXin Li 26*9c5db199SXin Li 27*9c5db199SXin Liclass Key(object): 28*9c5db199SXin Li """Enum for keyval label keys.""" 29*9c5db199SXin Li CROS_VERSION = 'cros-version' 30*9c5db199SXin Li CROS_ANDROID_VERSION = 'cheets-version' 31*9c5db199SXin Li FIRMWARE_RW_VERSION = 'fwrw-version' 32*9c5db199SXin Li FIRMWARE_RO_VERSION = 'fwro-version' 33*9c5db199SXin Li FIRMWARE_CR50_RW_VERSION = 'cr50-rw-version' 34*9c5db199SXin Li 35*9c5db199SXin Li 36*9c5db199SXin Liclass LabelsMapping(collections.MutableMapping): 37*9c5db199SXin Li """dict-like interface for working with labels. 38*9c5db199SXin Li 39*9c5db199SXin Li The constructor takes an iterable of labels, either plain or keyval. 40*9c5db199SXin Li Plain labels are saved internally and ignored except for converting 41*9c5db199SXin Li back to string labels. Keyval labels are exposed through a 42*9c5db199SXin Li dict-like interface (pop(), keys(), items(), etc. are all 43*9c5db199SXin Li supported). 44*9c5db199SXin Li 45*9c5db199SXin Li When multiple keyval labels share the same key, the first one wins. 46*9c5db199SXin Li 47*9c5db199SXin Li The one difference from a dict is that setting a key to None will 48*9c5db199SXin Li delete the corresponding keyval label, since it does not make sense 49*9c5db199SXin Li for a keyval label to have a None value. Prefer using del or pop() 50*9c5db199SXin Li instead of setting a key to None. 51*9c5db199SXin Li 52*9c5db199SXin Li LabelsMapping has one method getlabels() for converting back to 53*9c5db199SXin Li string labels. 54*9c5db199SXin Li """ 55*9c5db199SXin Li 56*9c5db199SXin Li def __init__(self, str_labels=()): 57*9c5db199SXin Li self._plain_labels = [] 58*9c5db199SXin Li self._keyval_map = collections.OrderedDict() 59*9c5db199SXin Li for str_label in str_labels: 60*9c5db199SXin Li self._add_label(str_label) 61*9c5db199SXin Li 62*9c5db199SXin Li def _add_label(self, str_label): 63*9c5db199SXin Li """Add a label string to the internal map or plain labels list.""" 64*9c5db199SXin Li try: 65*9c5db199SXin Li keyval_label = parse_keyval_label(str_label) 66*9c5db199SXin Li except ValueError: 67*9c5db199SXin Li self._plain_labels.append(str_label) 68*9c5db199SXin Li else: 69*9c5db199SXin Li if keyval_label.key not in self._keyval_map: 70*9c5db199SXin Li self._keyval_map[keyval_label.key] = keyval_label.value 71*9c5db199SXin Li 72*9c5db199SXin Li def __getitem__(self, key): 73*9c5db199SXin Li return self._keyval_map[key] 74*9c5db199SXin Li 75*9c5db199SXin Li def __setitem__(self, key, val): 76*9c5db199SXin Li if val is None: 77*9c5db199SXin Li self.pop(key, None) 78*9c5db199SXin Li else: 79*9c5db199SXin Li self._keyval_map[key] = val 80*9c5db199SXin Li 81*9c5db199SXin Li def __delitem__(self, key): 82*9c5db199SXin Li del self._keyval_map[key] 83*9c5db199SXin Li 84*9c5db199SXin Li def __iter__(self): 85*9c5db199SXin Li return iter(self._keyval_map) 86*9c5db199SXin Li 87*9c5db199SXin Li def __len__(self): 88*9c5db199SXin Li return len(self._keyval_map) 89*9c5db199SXin Li 90*9c5db199SXin Li def getlabels(self): 91*9c5db199SXin Li """Return labels as a list of strings.""" 92*9c5db199SXin Li str_labels = self._plain_labels[:] 93*9c5db199SXin Li keyval_labels = (KeyvalLabel(key, value) 94*9c5db199SXin Li for key, value in six.iteritems(self)) 95*9c5db199SXin Li str_labels.extend(format_keyval_label(label) 96*9c5db199SXin Li for label in keyval_labels) 97*9c5db199SXin Li return str_labels 98*9c5db199SXin Li 99*9c5db199SXin Li 100*9c5db199SXin Li_KEYVAL_LABEL_SEP = ':' 101*9c5db199SXin Li 102*9c5db199SXin Li 103*9c5db199SXin LiKeyvalLabel = collections.namedtuple('KeyvalLabel', 'key, value') 104*9c5db199SXin Li 105*9c5db199SXin Li 106*9c5db199SXin Lidef parse_keyval_label(str_label): 107*9c5db199SXin Li """Parse a string as a KeyvalLabel. 108*9c5db199SXin Li 109*9c5db199SXin Li If the argument is not a valid keyval label, ValueError is raised. 110*9c5db199SXin Li """ 111*9c5db199SXin Li key, value = str_label.split(_KEYVAL_LABEL_SEP, 1) 112*9c5db199SXin Li return KeyvalLabel(key, value) 113*9c5db199SXin Li 114*9c5db199SXin Li 115*9c5db199SXin Lidef format_keyval_label(keyval_label): 116*9c5db199SXin Li """Format a KeyvalLabel as a string.""" 117*9c5db199SXin Li return _KEYVAL_LABEL_SEP.join(keyval_label) 118*9c5db199SXin Li 119*9c5db199SXin Li 120*9c5db199SXin LiCrosVersion = collections.namedtuple( 121*9c5db199SXin Li 'CrosVersion', 'group, board, milestone, version, rc') 122*9c5db199SXin Li 123*9c5db199SXin Li 124*9c5db199SXin Li_CROS_VERSION_REGEX = ( 125*9c5db199SXin Li r'^' 126*9c5db199SXin Li r'(?P<group>[a-z0-9_-]+)' 127*9c5db199SXin Li r'/' 128*9c5db199SXin Li r'(?P<milestone>R[0-9]+)' 129*9c5db199SXin Li r'-' 130*9c5db199SXin Li r'(?P<version>[0-9.]+)' 131*9c5db199SXin Li r'(-(?P<rc>rc[0-9]+))?' 132*9c5db199SXin Li r'$' 133*9c5db199SXin Li) 134*9c5db199SXin Li 135*9c5db199SXin Li_CROS_BOARD_FROM_VERSION_REGEX = ( 136*9c5db199SXin Li r'^' 137*9c5db199SXin Li r'(trybot-)?' 138*9c5db199SXin Li r'(?P<board>[a-z_-]+)-(release|paladin|pre-cq|test-ap|toolchain)' 139*9c5db199SXin Li r'/R.*' 140*9c5db199SXin Li r'$' 141*9c5db199SXin Li) 142*9c5db199SXin Li 143*9c5db199SXin Li 144*9c5db199SXin Lidef parse_cros_version(version_string): 145*9c5db199SXin Li """Parse a string as a CrosVersion. 146*9c5db199SXin Li 147*9c5db199SXin Li If the argument is not a valid cros version, ValueError is raised. 148*9c5db199SXin Li Example cros version string: 'lumpy-release/R27-3773.0.0-rc1' 149*9c5db199SXin Li """ 150*9c5db199SXin Li match = re.search(_CROS_VERSION_REGEX, version_string) 151*9c5db199SXin Li if match is None: 152*9c5db199SXin Li raise ValueError('Invalid cros version string: %r' % version_string) 153*9c5db199SXin Li parts = match.groupdict() 154*9c5db199SXin Li match = re.search(_CROS_BOARD_FROM_VERSION_REGEX, version_string) 155*9c5db199SXin Li if match is None: 156*9c5db199SXin Li raise ValueError('Invalid cros version string: %r. Failed to parse ' 157*9c5db199SXin Li 'board.' % version_string) 158*9c5db199SXin Li parts['board'] = match.group('board') 159*9c5db199SXin Li return CrosVersion(**parts) 160*9c5db199SXin Li 161*9c5db199SXin Li 162*9c5db199SXin Lidef format_cros_version(cros_version): 163*9c5db199SXin Li """Format a CrosVersion as a string.""" 164*9c5db199SXin Li if cros_version.rc is not None: 165*9c5db199SXin Li return '{group}/{milestone}-{version}-{rc}'.format( 166*9c5db199SXin Li **cros_version._asdict()) 167*9c5db199SXin Li else: 168*9c5db199SXin Li return '{group}/{milestone}-{version}'.format(**cros_version._asdict()) 169