1# Copyright (c) 2017 Mark D. Hill and David A. Wood
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are
6# met: redistributions of source code must retain the above copyright
7# notice, this list of conditions and the following disclaimer;
8# redistributions in binary form must reproduce the above copyright
9# notice, this list of conditions and the following disclaimer in the
10# documentation and/or other materials provided with the distribution;
11# neither the name of the copyright holders nor the names of its
12# contributors may be used to endorse or promote products derived from
13# this software without specific prior written permission.
14#
15# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26#
27# Authors: Sean Wilson
28
29import multiprocessing.dummy
30import threading
31import traceback
32
33import helper
34import state
35import log
36import sandbox
37
38from state import Status, Result
39from fixture import SkipException
40
41def compute_aggregate_result(iterable):
42    '''
43    Status of the test suite by default is:
44    * Passed if all contained tests passed
45    * Errored if any contained tests errored
46    * Failed if no tests errored, but one or more failed.
47    * Skipped if all contained tests were skipped
48    '''
49    failed = []
50    skipped = []
51    for testitem in iterable:
52        result = testitem.result
53
54        if result.value == Result.Errored:
55            return Result(result.value, result.reason)
56        elif result.value == Result.Failed:
57            failed.append(result.reason)
58        elif result.value == result.Skipped:
59            skipped.append(result.reason)
60    if failed:
61        return Result(Result.Failed, failed)
62    elif skipped:
63        return Result(Result.Skipped, skipped)
64    else:
65        return Result(Result.Passed)
66
67class TestParameters(object):
68    def __init__(self, test, suite):
69        self.test = test
70        self.suite = suite
71        self.log = log.TestLogWrapper(log.test_log, test, suite)
72
73    @helper.cacheresult
74    def _fixtures(self):
75        fixtures = {fixture.name:fixture for fixture in self.suite.fixtures}
76        for fixture in self.test.fixtures:
77            fixtures[fixture.name] = fixture
78        return fixtures
79
80    @property
81    def fixtures(self):
82        return self._fixtures()
83
84
85class RunnerPattern:
86    def __init__(self, loaded_testable):
87        self.testable = loaded_testable
88        self.builder = FixtureBuilder(self.testable.fixtures)
89
90    def handle_error(self, trace):
91        self.testable.result = Result(Result.Errored, trace)
92        self.avoid_children(trace)
93
94    def handle_skip(self, trace):
95        self.testable.result = Result(Result.Skipped, trace)
96        self.avoid_children(trace)
97
98    def avoid_children(self, reason):
99        for testable in self.testable:
100            testable.result = Result(self.testable.result.value, reason)
101            testable.status = Status.Avoided
102
103    def test(self):
104        pass
105
106    def run(self):
107        avoided = False
108        try:
109            self.testable.status = Status.Building
110            self.builder.setup(self.testable)
111        except SkipException:
112            self.handle_skip(traceback.format_exc())
113            avoided = True
114        except BrokenFixtureException:
115            self.handle_error(traceback.format_exc())
116            avoided = True
117        else:
118            self.testable.status = Status.Running
119            self.test()
120        finally:
121            self.testable.status = Status.TearingDown
122            self.builder.teardown(self.testable)
123
124        if avoided:
125            self.testable.status = Status.Avoided
126        else:
127            self.testable.status = Status.Complete
128
129class TestRunner(RunnerPattern):
130    def test(self):
131        self.sandbox_test()
132
133    def sandbox_test(self):
134        try:
135            sandbox.Sandbox(TestParameters(
136                    self.testable,
137                    self.testable.parent_suite))
138        except sandbox.SubprocessException:
139            self.testable.result = Result(Result.Failed,
140                    traceback.format_exc())
141        else:
142            self.testable.result = Result(Result.Passed)
143
144
145class SuiteRunner(RunnerPattern):
146    def test(self):
147        for test in self.testable:
148            test.runner(test).run()
149        self.testable.result = compute_aggregate_result(
150                iter(self.testable))
151
152
153class LibraryRunner(SuiteRunner):
154    pass
155
156
157class LibraryParallelRunner(RunnerPattern):
158    def set_threads(self, threads):
159        self.threads = threads
160
161    def _entrypoint(self, suite):
162        suite.runner(suite).run()
163
164    def test(self):
165        pool = multiprocessing.dummy.Pool(self.threads)
166        pool.map(lambda suite : suite.runner(suite).run(), self.testable)
167        self.testable.result = compute_aggregate_result(
168                iter(self.testable))
169
170
171class BrokenFixtureException(Exception):
172    def __init__(self, fixture, testitem, trace):
173        self.fixture = fixture
174        self.testitem = testitem
175        self.trace = trace
176
177        self.msg = ('%s\n'
178                   'Exception raised building "%s" raised SkipException'
179                   ' for "%s".' %
180                   (trace, fixture.name, testitem.name)
181        )
182        super(BrokenFixtureException, self).__init__(self.msg)
183
184class FixtureBuilder(object):
185    def __init__(self, fixtures):
186        self.fixtures = fixtures
187        self.built_fixtures = []
188
189    def setup(self, testitem):
190        for fixture in self.fixtures:
191            # Mark as built before, so if the build fails
192            # we still try to tear it down.
193            self.built_fixtures.append(fixture)
194            try:
195                fixture.setup(testitem)
196            except SkipException:
197                raise
198            except Exception as e:
199                exc = traceback.format_exc()
200                msg = 'Exception raised while setting up fixture for %s' %\
201                        testitem.uid
202                log.test_log.warn('%s\n%s' % (exc, msg))
203
204                raise BrokenFixtureException(fixture, testitem,
205                        traceback.format_exc())
206
207    def teardown(self, testitem):
208        for fixture in self.built_fixtures:
209            try:
210                fixture.teardown(testitem)
211            except Exception:
212                # Log exception but keep cleaning up.
213                exc = traceback.format_exc()
214                msg = 'Exception raised while tearing down fixture for %s' %\
215                        testitem.uid
216                log.test_log.warn('%s\n%s' % (exc, msg))
217