summaryrefslogtreecommitdiff
path: root/buildscripts/ciconfig/tags.py
blob: 7e9688714f572e4c79b543f83b2416bc007e6fb6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""Module to access and modify tag configuration files used by resmoke."""

from __future__ import absolute_import
from __future__ import print_function

import collections
import copy
import textwrap

import yaml


# Setup to preserve order in yaml.dump, see https://stackoverflow.com/a/8661021
def _represent_dict_order(self, data):
    return self.represent_mapping("tag:yaml.org,2002:map", data.items())


yaml.add_representer(collections.OrderedDict, _represent_dict_order)

# End setup


class TagsConfig(object):
    """Represent a test tag configuration file."""

    def __init__(self, raw, cmp_func=None):
        """Initialize a TagsConfig from a dict representing the associations between tests and tags.

        'cmp_func' can be used to specify a comparison function that will be used when sorting tags.
        """

        self.raw = raw
        self._conf = self.raw["selector"]
        self._conf_copy = copy.deepcopy(self._conf)
        self._cmp_func = cmp_func

    @classmethod
    def from_file(cls, filename, **kwargs):
        """Return a TagsConfig from a file containing the associations between tests and tags.

        See TagsConfig.__init__() for the keyword arguments that can be specified.
        """

        with open(filename, "r") as fstream:
            raw = yaml.safe_load(fstream)

        return cls(raw, **kwargs)

    @classmethod
    def from_dict(cls, raw, **kwargs):
        """Return a TagsConfig from a dict representing the associations between tests and tags.

        See TagsConfig.__init__() for the keyword arguments that can be specified.
        """

        return cls(copy.deepcopy(raw), **kwargs)

    def get_test_kinds(self):
        """List the test kinds."""
        return self._conf.keys()

    def get_test_patterns(self, test_kind):
        """List the test patterns under 'test_kind'."""
        return getdefault(self._conf, test_kind, {}).keys()

    def get_tags(self, test_kind, test_pattern):
        """List the tags under 'test_kind' and 'test_pattern'."""
        patterns = getdefault(self._conf, test_kind, {})
        return getdefault(patterns, test_pattern, [])

    def add_tag(self, test_kind, test_pattern, tag):
        """Add a tag. Return True if the tag is added or False if the tag was already present."""
        patterns = setdefault(self._conf, test_kind, {})
        tags = setdefault(patterns, test_pattern, [])
        if tag not in tags:
            tags.append(tag)
            tags.sort(cmp=self._cmp_func)
            return True
        return False

    def remove_tag(self, test_kind, test_pattern, tag):
        """Remove a tag. Return True if the tag was removed or False if the tag was not present."""
        patterns = self._conf.get(test_kind)
        if not patterns or test_pattern not in patterns:
            return False
        tags = patterns.get(test_pattern)
        if tags and tag in tags:
            tags[:] = (value for value in tags if value != tag)
            # Remove the pattern if there are no associated tags.
            if not tags:
                del patterns[test_pattern]
            return True
        return False

    def remove_test_pattern(self, test_kind, test_pattern):
        """Remove a test pattern."""
        patterns = self._conf.get(test_kind)
        if not patterns or test_pattern not in patterns:
            return
        del patterns[test_pattern]

    def is_modified(self):
        """Return True if the tags have been modified, False otherwise."""
        return self._conf != self._conf_copy

    def write_file(self, filename, preamble=None):
        """Write the tags to a file.

        If 'preamble' is present it will be added as a comment at the top of the file.
        """
        with open(filename, "w") as fstream:
            if preamble:
                print(textwrap.fill(preamble, width=100, initial_indent="# ",
                                    subsequent_indent="# "), file=fstream)

            # We use yaml.safe_dump() in order avoid having strings being written to the file as
            # "!!python/unicode ..." and instead have them written as plain 'str' instances.
            yaml.safe_dump(self.raw, fstream, default_flow_style=False)


def getdefault(doc, key, default):
    """Return the value in 'doc' with key 'key' if it is present and not None, returns
    the specified default value otherwise."""
    value = doc.get(key)
    if value is not None:
        return value
    else:
        return default


def setdefault(doc, key, default):
    """Return the value in 'doc' with key 'key' if it is present and not None, sets the value
    to default and return it otherwise."""
    value = doc.setdefault(key, default)
    if value is not None:
        return value
    else:
        doc[key] = default
        return default