Home | History | Annotate | Download | only in utils
      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     FIRMWARE_RW_VERSION = 'fwrw-version'
     27     FIRMWARE_RO_VERSION = 'fwro-version'
     28 
     29 
     30 class 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 
    102 KeyvalLabel = collections.namedtuple('KeyvalLabel', 'key, value')
    103 
    104 
    105 def 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 
    114 def format_keyval_label(keyval_label):
    115     """Format a KeyvalLabel as a string."""
    116     return _KEYVAL_LABEL_SEP.join(keyval_label)
    117 
    118 
    119 CrosVersion = 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 
    143 def 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 
    161 def 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