summaryrefslogtreecommitdiff
path: root/framework/profile.py
blob: 8888c77bb08e2c88cff5fc0268d50547061aa92a (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
291
292
293
294
295
296
297
298
299
300
301
302
303
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# This permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

""" Provides Profiles for test groups

Each set of tests, both native Piglit profiles and external suite integration,
are represented by a TestProfile or a TestProfile derived object.

"""

from __future__ import print_function
import os
import sys
import multiprocessing
import multiprocessing.dummy
import importlib

from framework.dmesg import get_dmesg
from framework.log import LogManager
import framework.exectest

__all__ = [
    'TestProfile',
    'load_test_profile',
    'merge_test_profiles'
]


class TestProfile(object):
    """ Class that holds a list of tests for execution

    This class provides a container for storing tests in either a nested
    dictionary structure (deprecated), or a flat dictionary structure with '/'
    delimited groups.

    Once a TestProfile object is created tests can be added to the test_list
    name as a key/value pair, the key should be a fully qualified name for the
    test, including it's group hierarchy and should be '/' delimited, with no
    leading or trailing '/', the value should be an exectest.Test derived
    object.

    When the test list is filled calling TestProfile.run() will set the
    execution of these tests off, and will flatten the nested group hierarchy
    of self.tests and merge it with self.test_list

    """
    def __init__(self):
        # Self.tests is deprecated, see above
        self.tests = {}
        self.test_list = {}
        self.filters = []
        # Sets a default of a Dummy
        self._dmesg = None
        self.dmesg = False
        self.results_dir = None

    @property
    def dmesg(self):
        """ Return dmesg """
        return self._dmesg

    @dmesg.setter
    def dmesg(self, not_dummy):
        """ Set dmesg

        Arguments:
        not_dummy -- if Truthy dmesg will try to get a PosixDmesg, if Falsy it
                     will get a DummyDmesg

        """
        self._dmesg = get_dmesg(not_dummy)

    def _flatten_group_hierarchy(self):
        """ Flatten nested dictionary structure

        Convert Piglit's old hierarchical Group() structure into a flat
        dictionary mapping from fully qualified test names to "Test" objects.

        For example,
        self.tests['spec']['glsl-1.30']['preprocessor']['compiler']['void.frag']
        would become:
        self.test_list['spec/glsl-1.30/preprocessor/compiler/void.frag']

        """

        def f(prefix, group, test_dict):
            """ Recursively flatter nested dictionary tree """
            for key, value in group.iteritems():
                fullkey = os.path.join(prefix, key)
                if isinstance(value, dict):
                    f(fullkey, value, test_dict)
                else:
                    test_dict[fullkey] = value
        f('', self.tests, self.test_list)
        # Clear out the old Group()
        self.tests = {}

    def _prepare_test_list(self, opts):
        """ Prepare tests for running

        Flattens the nested group hierarchy into a flat dictionary using '/'
        delimited groups by calling self.flatten_group_hierarchy(), then
        runs it's own filters plus the filters in the self.filters name

        Arguments:
        opts - a core.Options instance

        """
        self._flatten_group_hierarchy()

        def matches_any_regexp(x, re_list):
            return any(r.search(x) for r in re_list)

        # The extra argument is needed to match check_all's API
        def test_matches(path, test):
            """Filter for user-specified restrictions"""
            return ((not opts.filter or matches_any_regexp(path, opts.filter))
                    and not path in opts.exclude_tests and
                    not matches_any_regexp(path, opts.exclude_filter))

        filters = self.filters + [test_matches]
        def check_all(item):
            """ Checks group and test name against all filters """
            path, test = item
            for f in filters:
                if not f(path, test):
                    return False
            return True

        # Filter out unwanted tests
        self.test_list = dict(item for item in self.test_list.iteritems()
                              if check_all(item))

    def _pre_run_hook(self):
        """ Hook executed at the start of TestProfile.run

        To make use of this hook one will need to subclass TestProfile, and
        set this to do something, as be default it will no-op

        """
        pass

    def _post_run_hook(self):
        """ Hook executed at the end of TestProfile.run

        To make use of this hook one will need to subclass TestProfile, and
        set this to do something, as be default it will no-op

        """
        pass

    def run(self, opts, logger, backend):
        """ Runs all tests using Thread pool

        When called this method will flatten out self.tests into
        self.test_list, then will prepare a logger, pass opts to the Test
        class, and begin executing tests through it's Thread pools.

        Based on the value of opts.concurrent it will either run all the tests
        concurrently, all serially, or first the thread safe tests then the
        serial tests.

        Finally it will print a final summary of the tests

        Arguments:
        opts -- a core.Options instance
        backend -- a results.Backend derived instance
        

        """

        self._pre_run_hook()
        framework.exectest.Test.OPTS = opts

        chunksize = 1

        self._prepare_test_list(opts)
        log = LogManager(logger, len(self.test_list))

        def test(pair):
            """ Function to call test.execute from .map

            Adds opts which are needed by Test.execute()

            """
            name, test = pair
            test.execute(name, log.get(), self.dmesg)
            backend.write_test(name, test.result)

        def run_threads(pool, testlist):
            """ Open a pool, close it, and join it """
            pool.imap(test, testlist, chunksize)
            pool.close()
            pool.join()

        # Multiprocessing.dummy is a wrapper around Threading that provides a
        # multiprocessing compatible API
        #
        # The default value of pool is the number of virtual processor cores
        single = multiprocessing.dummy.Pool(1)
        multi = multiprocessing.dummy.Pool()

        if opts.concurrent == "all":
            run_threads(multi, self.test_list.iteritems())
        elif opts.concurrent == "none":
            run_threads(single, self.test_list.iteritems())
        else:
            # Filter and return only thread safe tests to the threaded pool
            run_threads(multi, (x for x in self.test_list.iteritems()
                                if x[1].run_concurrent))
            # Filter and return the non thread safe tests to the single pool
            run_threads(single, (x for x in self.test_list.iteritems()
                                 if not x[1].run_concurrent))

        log.get().summary()

        self._post_run_hook()

    def filter_tests(self, function):
        """Filter out tests that return false from the supplied function

        Arguments:
        function -- a callable that takes two parameters: path, test and
                    returns whether the test should be included in the test
                    run or not.
        """
        self.filters.append(function)

    def update(self, *profiles):
        """ Updates the contents of this TestProfile instance with another

        This method overwrites key:value pairs in self with those in the
        provided profiles argument. This allows multiple TestProfiles to be
        called in the same run; which could be used to run piglit and external
        suites at the same time.

        Arguments:
        profiles -- one or more TestProfile-like objects to be merged.

        """
        for profile in profiles:
            self.tests.update(profile.tests)
            self.test_list.update(profile.test_list)


def load_test_profile(filename):
    """ Load a python module and return it's profile attribute

    All of the python test files provide a profile attribute which is a
    TestProfile instance. This loads that module and returns it or raises an
    error.

    This method doesn't care about file extensions as a way to be backwards
    compatible with script wrapping piglit. 'tests/quick', 'tests/quick.tests',
    and 'tests/quick.py' are all equally valid for filename

    Arguments:
    filename -- the name of a python module to get a 'profile' from

    """
    mod = importlib.import_module('tests.{0}'.format(
        os.path.splitext(os.path.basename(filename))[0]))

    try:
        return mod.profile
    except AttributeError:
        print("Error: There is not profile attribute in module {0}."
              "Did you specify the right file?".format(filename))
        sys.exit(1)


def merge_test_profiles(profiles):
    """ Helper for loading and merging TestProfile instances

    Takes paths to test profiles as arguments and returns a single merged
    TestProfile instance.

    Arguments:
    profiles -- a list of one or more paths to profile files.

    """
    profile = load_test_profile(profiles.pop())
    for p in profiles:
        profile.update(load_test_profile(p))
    return profile