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