summaryrefslogtreecommitdiff
path: root/chromium/third_party/blink/tools/blinkpy/web_tests/models/test_configuration.py
blob: c4745d6b1cec53c025ed3c51a45dffd46667923e (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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# Copyright (C) 2011 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the Google name nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import copy
import itertools
from functools import reduce


class TestConfiguration(object):

    def __init__(self, version, architecture, build_type):
        self.version = version
        self.architecture = architecture
        self.build_type = build_type

    @classmethod
    def category_order(cls):
        """The most common human-readable order in which the configuration properties are listed."""
        return ['version', 'architecture', 'build_type']

    def items(self):
        return self.__dict__.items()

    def keys(self):
        return self.__dict__.keys()

    def __str__(self):
        return ('<%(version)s, %(architecture)s, %(build_type)s>' %
                self.__dict__)

    def __repr__(self):
        return "TestConfig(version='%(version)s', architecture='%(architecture)s', build_type='%(build_type)s')" % self.__dict__

    def __hash__(self):
        return hash(self.version + self.architecture + self.build_type)

    def __eq__(self, other):
        return self.__hash__() == other.__hash__()

    def values(self):
        """Returns the configuration values of this instance as a tuple."""
        return self.__dict__.values()


class SpecifierSorter(object):

    def __init__(self, all_test_configurations=None, macros=None):
        self._specifier_to_category = {}

        if not all_test_configurations:
            return
        for test_configuration in all_test_configurations:
            for category, specifier in test_configuration.items():
                self.add_specifier(category, specifier)

        self.add_macros(macros)

    def add_specifier(self, category, specifier):
        self._specifier_to_category[specifier] = category

    def add_macros(self, macros):
        if not macros:
            return
        # Assume well-formed macros.
        for macro, specifier_list in macros.items():
            self.add_specifier(self.category_for_specifier(specifier_list[0]), macro)

    @classmethod
    def category_priority(cls, category):
        return TestConfiguration.category_order().index(category)

    def specifier_priority(self, specifier):
        return self.category_priority(self._specifier_to_category[specifier])

    def category_for_specifier(self, specifier):
        return self._specifier_to_category.get(specifier)

    def sort_specifiers(self, specifiers):
        category_slots = map(lambda x: [], TestConfiguration.category_order())
        for specifier in specifiers:
            category_slots[self.specifier_priority(specifier)].append(specifier)

        def sort_and_return(result, specifier_list):
            specifier_list.sort()
            return result + specifier_list

        return reduce(sort_and_return, category_slots, [])


class TestConfigurationConverter(object):

    def __init__(self, all_test_configurations, configuration_macros=None):
        self._all_test_configurations = all_test_configurations
        self._configuration_macros = configuration_macros or {}
        self._specifier_to_configuration_set = {}
        self._specifier_sorter = SpecifierSorter()
        self._collapsing_sets_by_size = {}
        self._junk_specifier_combinations = {}
        self._collapsing_sets_by_category = {}
        matching_sets_by_category = {}
        for configuration in all_test_configurations:
            for category, specifier in configuration.items():
                self._specifier_to_configuration_set.setdefault(specifier, set()).add(configuration)
                self._specifier_sorter.add_specifier(category, specifier)
                self._collapsing_sets_by_category.setdefault(category, set()).add(specifier)
                # FIXME: This seems extra-awful.
                for cat2, spec2 in configuration.items():
                    if category == cat2:
                        continue
                    matching_sets_by_category.setdefault(specifier, {}).setdefault(cat2, set()).add(spec2)
        for collapsing_set in self._collapsing_sets_by_category.values():
            self._collapsing_sets_by_size.setdefault(len(collapsing_set), set()).add(frozenset(collapsing_set))

        for specifier, sets_by_category in matching_sets_by_category.items():
            for category, set_by_category in sets_by_category.items():
                if len(set_by_category) == 1 and self._specifier_sorter.category_priority(
                        category) > self._specifier_sorter.specifier_priority(specifier):
                    self._junk_specifier_combinations[specifier] = set_by_category

        self._specifier_sorter.add_macros(configuration_macros)

    def specifier_sorter(self):
        return self._specifier_sorter

    def _expand_macros(self, specifier):
        expanded_specifiers = self._configuration_macros.get(specifier)
        return expanded_specifiers or [specifier]

    def to_config_set(self, specifier_set, error_list=None):
        """Convert a list of specifiers into a set of TestConfiguration instances."""
        if len(specifier_set) == 0:
            return copy.copy(self._all_test_configurations)

        matching_sets = {}

        for specifier in specifier_set:
            for expanded_specifier in self._expand_macros(specifier):
                configurations = self._specifier_to_configuration_set.get(expanded_specifier)
                if not configurations:
                    if error_list is not None:
                        error_list.append("Unrecognized specifier '" + expanded_specifier + "'")
                    return set()
                category = self._specifier_sorter.category_for_specifier(expanded_specifier)
                matching_sets.setdefault(category, set()).update(configurations)

        return reduce(set.intersection, matching_sets.values())

    @classmethod
    def collapse_macros(cls, macros_dict, specifiers_list):
        for macro_specifier, macro in macros_dict.items():
            if len(macro) == 1:
                continue

            for combination in itertools.combinations(specifiers_list, len(macro)):
                if cls.symmetric_difference(combination) == set(macro):
                    for item in combination:
                        specifiers_list.remove(item)
                    new_specifier_set = cls.intersect_combination(combination)
                    new_specifier_set.add(macro_specifier)
                    specifiers_list.append(frozenset(new_specifier_set))

        def collapse_individual_specifier_set(macro_specifier, macro):
            specifiers_to_remove = []
            specifiers_to_add = []
            for specifier_set in specifiers_list:
                macro_set = set(macro)
                if macro_set.intersection(specifier_set) == macro_set:
                    specifiers_to_remove.append(specifier_set)
                    specifiers_to_add.append(frozenset((set(specifier_set) - macro_set) | set([macro_specifier])))
            for specifier in specifiers_to_remove:
                specifiers_list.remove(specifier)
            for specifier in specifiers_to_add:
                specifiers_list.append(specifier)

        for macro_specifier, macro in macros_dict.items():
            collapse_individual_specifier_set(macro_specifier, macro)

    @classmethod
    def intersect_combination(cls, combination):
        return reduce(set.intersection, [set(specifiers) for specifiers in combination])

    @classmethod
    def symmetric_difference(cls, iterable):
        union = set()
        intersection = iterable[0]
        for item in iterable:
            union = union | item
            intersection = intersection.intersection(item)
        return union - intersection

    def to_specifiers_list(self, test_configuration_set):
        """Convert a set of TestConfiguration instances into one or more list of specifiers."""
        # Easy out: if the set is all configurations, the specifier is empty.
        if len(test_configuration_set) == len(self._all_test_configurations):
            return [[]]

        # 1) Build a list of specifier sets, discarding specifiers that don't add value.
        specifiers_list = []
        for config in test_configuration_set:
            values = set(config.values())
            for specifier, junk_specifier_set in self._junk_specifier_combinations.items():
                if specifier in values:
                    values -= junk_specifier_set
            specifiers_list.append(frozenset(values))

        def try_collapsing(size, collapsing_sets):
            if len(specifiers_list) < size:
                return False
            for combination in itertools.combinations(specifiers_list, size):
                if self.symmetric_difference(combination) in collapsing_sets:
                    for item in combination:
                        specifiers_list.remove(item)
                    specifiers_list.append(frozenset(self.intersect_combination(combination)))
                    return True
            return False

        # 2) Collapse specifier sets with common specifiers:
        #   (win7, release), (win7, debug) --> (win7, x86)
        for size, collapsing_sets in self._collapsing_sets_by_size.items():
            while try_collapsing(size, collapsing_sets):
                pass

        def try_abbreviating(collapsing_sets):
            if len(specifiers_list) < 2:
                return False
            for combination in itertools.combinations(specifiers_list, 2):
                for collapsing_set in collapsing_sets:
                    diff = self.symmetric_difference(combination)
                    if diff <= collapsing_set:
                        common = self.intersect_combination(combination)
                        for item in combination:
                            specifiers_list.remove(item)
                        specifiers_list.append(frozenset(common | diff))
                        return True
            return False

        # 3) Abbreviate specifier sets by combining specifiers across categories.
        #   (win7, release), (win10, release) --> (win7, win10, release)
        while try_abbreviating(self._collapsing_sets_by_size.values()):
            pass

        # 4) Substitute specifier subsets that match macros within each set:
        #   (win7, win10, release) -> (win, release)
        self.collapse_macros(self._configuration_macros, specifiers_list)

        macro_keys = set(self._configuration_macros.keys())

        # 5) Collapsing macros may have created combinations the can now be abbreviated.
        #   (win7, release), (linux, x86, release), (linux, x86_64, release)
        #   --> (win7, release), (linux, release) --> (win7, linux, release)
        while try_abbreviating([self._collapsing_sets_by_category['version'] | macro_keys]):
            pass

        # 6) Remove cases where we have collapsed but have all macros.
        #   (android, win, mac, linux, release) --> (release)
        specifiers_to_remove = []
        for specifier_set in specifiers_list:
            if macro_keys <= specifier_set:
                specifiers_to_remove.append(specifier_set)

        for specifier_set in specifiers_to_remove:
            specifiers_list.remove(specifier_set)
            specifiers_list.append(frozenset(specifier_set - macro_keys))

        return specifiers_list