Completed
Pull Request — master (#177)
by Erin
02:08
created

Test::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 10
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
1
<?php
2
3
namespace Peridot\Core;
4
5
use ErrorException;
6
use Exception;
7
use Throwable;
8
9
/**
10
 * The main test fixture for Peridot.
11
 *
12
 * @package Peridot\Core
13
 */
14
class Test extends AbstractTest
15
{
16
    /**
17
     * @param string $description
18
     * @param callable $definition
19
     */
20
    public function __construct($description, callable $definition = null)
21
    {
22
        if ($definition === null) {
23
            $this->pending = true;
24
            $definition = function () {
25
                //noop
26
            };
27
        }
28
        parent::__construct($description, $definition);
29
    }
30
31
    /**
32
     * Execute the test along with any setup and tear down functions.
33
     *
34
     * @param  TestResult $result
35
     * @return void
36
     */
37
    public function run(TestResult $result)
38
    {
39
        $result->startTest($this);
40
41
        if ($this->getPending()) {
42
            $result->pendTest($this);
43
            return;
44
        }
45
        $this->executeTest($result);
46
        $result->endTest($this);
47
    }
48
49
    /**
50
     * Attempt to execute setup functions and run the test definition
51
     *
52
     * @param TestResult $result
53
     */
54
    protected function executeTest(TestResult $result)
55
    {
56
        $action = ['passTest', $this];
57
        $handlers = $this->handleErrors($result, $action);
58
        try {
59
            $this->runSetup();
60
            call_user_func_array($this->getDefinition(), $this->getDefinitionArguments());
61
        } catch (Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
62
            if ('passTest' === $action[0]) {
63
                $action = ['failTest', $this, $e];
64
            }
65
        } catch (Exception $e) {
66
            if ('passTest' === $action[0]) {
67
                $action = ['failTest', $this, $e];
68
            }
69
        }
70
        $this->runTearDown($result, $action);
71
        $this->restoreErrorHandlers($handlers);
72
    }
73
74
    /**
75
     * Excecute the test's setup functions
76
     */
77
    protected function runSetup()
78
    {
79
        $this->forEachNodeTopDown(function (TestInterface $node) {
80
            $setups = $node->getSetupFunctions();
81
            foreach ($setups as $setup) {
82
                $setup();
83
            }
84
        });
85
    }
86
87
    /**
88
     * Run the tests tear down methods and have the result
89
     * perform the method indicated by $action
90
     *
91
     * @param TestResult $result
92
     * @param array $action
93
     */
94
    protected function runTearDown(TestResult $result, $action)
95
    {
96
        $this->forEachNodeBottomUp(function (TestInterface $test) use ($result, &$action) {
97
            $tearDowns = $test->getTearDownFunctions();
98
            foreach ($tearDowns as $tearDown) {
99
                try {
100
                    $tearDown();
101
                } catch (Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
102
                    if ('passTest' === $action[0]) {
103
                        $action = ['failTest', $this, $e];
104
                    }
105
                } catch (Exception $e) {
106
                    if ('passTest' === $action[0]) {
107
                        $action = ['failTest', $this, $e];
108
                    }
109
                }
110
            }
111
        });
112
        call_user_func_array([$result, $action[0]], array_slice($action, 1));
113
    }
114
115
    /**
116
     * Set an error handler to handle errors within the test
117
     *
118
     * @param TestResult $result
119
     * @param array      &$action
120
     *
121
     * @return array
122
     */
123
    protected function handleErrors(TestResult $result, &$action)
124
    {
125
        // first, unwind the current handler stack for later restoration
126
        $handlers = [];
127
128
        do {
129
            if ($handler = set_error_handler(function () {})) {
130
                $handlers[] = $handler;
131
                restore_error_handler();
132
            }
133
134
            restore_error_handler();
135
        } while ($handler);
136
137
        // determine the most recently added handler
138
        if ($handlers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $handlers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
139
            $handler = $handlers[0];
140
        } else {
141
            $handler = null;
142
        }
143
144
        set_error_handler(function ($severity, $message, $path, $line) use ($result, &$action, &$handler) {
145
            // if there is an existing error handler, call it and record the result
146
            $isHandled = $handler && false !== $handler($severity, $message, $path, $line);
147
148
            if (!$isHandled) {
149
                $result->getEventEmitter()->emit('error', [$severity, $message, $path, $line]);
150
151
                // honor the error reporting configuration - this also takes care of the error control operator (@)
152
                $errorReporting = error_reporting();
153
                $shouldHandle = $severity === ($severity & $errorReporting);
154
155
                if ($shouldHandle && 'passTest' === $action[0]) {
156
                    $action = ['failTest', $this, new ErrorException($message, 0, $severity, $path, $line)];
157
                }
158
            }
159
        });
160
161
        return $handlers;
162
    }
163
164
    /**
165
     * Restore the error handler stack to a previous state
166
     *
167
     * @param array $handlers
168
     */
169
    protected function restoreErrorHandlers($handlers)
170
    {
171
        // discard existing handlers
172
        do {
173
            if ($handler = set_error_handler(function () {})) {
174
                restore_error_handler();
175
            }
176
177
            restore_error_handler();
178
        } while ($handler);
179
180
        // restore handlers in reverse order
181
        for ($i = count($handlers) - 1; $i >= 0; --$i) {
182
            set_error_handler($handlers[$i]);
183
        }
184
    }
185
}
186