xref: /aosp_15_r20/external/autotest/utils/labellib.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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