Issues (300)

src/Block/Group.php (3 issues)

1
<?php
2
namespace Kahlan\Block;
3
4
use Kahlan\Block;
5
use Closure;
6
use Exception;
7
use Throwable;
8
use Kahlan\Suite;
9
use Kahlan\Scope\Group as Scope;
10
11
class Group extends Block
12
{
13
    /**
14
     * The each callbacks.
15
     *
16
     * @var array
17
     */
18
    protected $_callbacks = [
19
        'beforeAll'  => [],
20
        'afterAll'   => [],
21
        'beforeEach' => [],
22
        'afterEach'  => [],
23
    ];
24
25
    /**
26
     * Indicates if the group has been loaded or not.
27
     *
28
     * @var boolean
29
     */
30
    protected $_loaded = false;
31
32
    /**
33
     * The children array.
34
     *
35
     * @var Group[]|Specification[]
36
     */
37
    protected $_children = [];
38
39
    /**
40
     * Group statistics.
41
     *
42
     * @var array
43
     */
44
    protected $_stats = null;
45
46
    /**
47
     * Group state.
48
     *
49
     * @var array
50
     */
51
    protected $_enabled = true;
52
53
    /**
54
     * The Constructor.
55
     *
56
     * @param array $config The Group config array. Options are:
57
     *                      -`'name'`    _string_ : the type of the suite.
58
     */
59
    public function __construct($config = [])
60 121
    {
61
        parent::__construct($config);
62 121
63 121
        $this->_scope = new Scope(['block' => $this]);
64
        $this->_closure = $this->_bindScope($this->_closure);
65
    }
66
67
    /**
68
     * Gets children.
69
     *
70
     * @return array The array of children instances.
71
     */
72
    public function children()
73 48
    {
74
        return $this->_children;
75
    }
76
77
    /**
78
     * Builds the group stats.
79
     *
80
     * @return array The group stats.
81
     */
82
    public function stats()
83
    {
84
        if ($this->_stats !== null) {
85
            return $this->_stats;
86
        }
87 40
88
        Suite::push($this);
89
90 40
        $builder = function ($block) {
91 40
            $block->load();
92 40
            $normal = 0;
93 40
            $inactive = 0;
94 40
            $focused = 0;
95
            $excluded = 0;
96
97
            foreach ($block->children() as $child) {
98 4
                if ($block->excluded()) {
99
                    $child->type('exclude');
100
                }
101 38
                if ($child instanceof Group) {
102
                    $result = $child->stats();
103 6
                    if ($child->focused() && !$result['focused']) {
104 6
                        $focused += $result['normal'];
105 6
                        $excluded += $result['excluded'];
106
                        $child->broadcastFocus();
107 38
                    } elseif (!$child->enabled()) {
108
                        $inactive += $result['normal'];
109
                        $focused += $result['focused'];
110
                        $excluded += $result['excluded'];
111 38
                    } else {
112 38
                        $normal += $result['normal'];
113 38
                        $focused += $result['focused'];
114
                        $excluded += $result['excluded'];
115
                    }
116
                } else {
117
                    switch ($child->type()) {
118 6
                        case 'exclude':
119 6
                            $excluded++;
120
                            break;
121 12
                        case 'focus':
122 12
                            $focused++;
123
                            break;
124 36
                        default:
125 36
                            $normal++;
126
                            break;
127
                    }
128
                }
129 40
            }
130
            return compact('normal', 'inactive', 'focused', 'excluded');
131
        };
132
133 40
        try {
134
            $stats = $builder($this);
135 2
        } catch (Throwable $exception) {
136 2
            $this->log()->type('errored');
137
            $this->log()->exception($exception);
138
139
            $stats = [
140
                'normal' => 0,
141
                'focused' => 0,
142 2
                'excluded' => 0
143
            ];
144
        }
145 40
146 40
        Suite::pop();
147
        return $stats;
148
    }
149
150
    /**
151
     * Splits the specs in different partitions and only enable one.
152
     *
153
     * @param integer $index The partition index to enable.
154
     * @param integer $total The total of partitions.
155
     */
156
    public function partition($index, $total)
157 38
    {
158 38
        $index = (integer) $index;
159
        $total = (integer) $total;
160 38
        if (!$index || !$total || $index > $total) {
161
            throw new Exception("Invalid partition parameters: {$index}/{$total}");
162
        }
163 38
164 38
        $groups = [];
165 38
        $partitions = [];
166
        $partitionsTotal = [];
167 38
168 38
        for ($i = 0; $i < $total; $i++) {
169 38
            $partitions[$i] = [];
170
            $partitionsTotal[$i] = 0;
171
        }
172 38
173
        $children = $this->children();
174
175 38
        foreach ($children as $key => $child) {
176 38
            $groups[$key] = $child->stats()['normal'];
177
            $child->enabled(false);
178 38
        }
179
        asort($groups);
180
181 38
        foreach ($groups as $key => $value) {
182 38
            $i = array_search(min($partitionsTotal), $partitionsTotal);
183 38
            $partitions[$i][] = $key;
184
            $partitionsTotal[$i] += $groups[$key];
185
        }
186
187 38
        foreach ($partitions[$index - 1] as $key) {
188
            $children[$key]->enabled(true);
189
        }
190
    }
191
192
    /**
193
     * Set/get the enable value.
194
     *
195
     * @param  string $enable The enable value.
196
     * @return mixed
197
     */
198
    public function enabled($enable = null)
199
    {
200 1265
        if (!func_num_args()) {
201
            return $this->_enabled;
202 38
        }
203 38
        $this->_enabled = $enable;
0 ignored issues
show
Documentation Bug introduced by
It seems like $enable can also be of type string. However, the property $_enabled is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
204
        return $this;
205
    }
206
207
    /* Adds a group/class related spec.
208
     *
209
     * @param  string  $message Description message.
210
     * @param  Closure $closure A test case closure.
211
     *
212
     * @return Group
213
     */
214
    public function describe($message, $closure, $timeout = null, $type = 'normal')
215 44
    {
216 44
        $suite = $this->suite();
217 44
        $parent = $this;
218 44
        $timeout = $timeout ?? $this->timeout();
219
        $group = new Group(compact('message', 'closure', 'suite', 'parent', 'timeout', 'type'));
220 44
221
        return $this->_children[] = $group;
222
    }
223
224
    /**
225
     * Adds a context related spec.
226
     *
227
     * @param  string  $message Description message.
228
     * @param  Closure $closure A test case closure.
229
     * @param  null    $timeout
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $timeout is correct as it would always require null to be passed?
Loading history...
230
     * @param  string  $type
231
     *
232
     * @return Group
233
     */
234
    public function context($message, $closure, $timeout = null, $type = 'normal')
235 6
    {
236
        return $this->describe($message, $closure, $timeout, $type);
237
    }
238
239
    /**
240
     * Adds a spec.
241
     *
242
     * @param  string|Closure $message Description message or a test closure.
243
     * @param  Closure        $closure A test case closure.
244
     * @param  string         $type    The type.
245
     *
246
     * @return Specification
247
     */
248
    public function it($message, $closure = null, $timeout = null, $type = 'normal')
249 38
    {
250 38
        $suite = $this->suite();
251 38
        $parent = $this;
252 38
        $timeout = $timeout ?? $this->timeout();
253 38
        $spec = new Specification(compact('message', 'closure', 'suite', 'parent', 'timeout', 'type'));
254
        $this->_children[] = $spec;
255 38
256
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type Kahlan\Block\Group which is incompatible with the documented return type Kahlan\Block\Specification.
Loading history...
257
    }
258
259
    /**
260
     * Executed before tests.
261
     *
262
     * @param  Closure $closure A closure
263
     *
264
     * @return self
265
     */
266
    public function beforeAll($closure)
267 8
    {
268 8
        $this->_callbacks['beforeAll'][] = $this->_bindScope($closure);
269
        return $this;
270
    }
271
272
    /**
273
     * Executed after tests.
274
     *
275
     * @param  Closure $closure A closure
276
     *
277
     * @return self
278
     */
279
    public function afterAll($closure)
280 8
    {
281 8
        array_unshift($this->_callbacks['afterAll'], $this->_bindScope($closure));
282
        return $this;
283
    }
284
285
    /**
286
     * Executed before each tests.
287
     *
288
     * @param  Closure $closure A closure
289
     *
290
     * @return self
291
     */
292
    public function beforeEach($closure)
293 10
    {
294 10
        $this->_callbacks['beforeEach'][] = $this->_bindScope($closure);
295
        return $this;
296
    }
297
298
    /**
299
     * Executed after each tests.
300
     *
301
     * @param  Closure $closure A closure
302
     *
303
     * @return self
304
     */
305
    public function afterEach($closure)
306 10
    {
307 10
        array_unshift($this->_callbacks['afterEach'], $this->_bindScope($closure));
308
        return $this;
309
    }
310
311
    /**
312
     * Load the group.
313
     */
314
    public function load()
315
    {
316 40
        if ($this->_loaded) {
317
            return;
318 40
        }
319
        $this->_loaded = true;
320
        if (!$closure = $this->closure()) {
321
            return;
322 40
        }
323
        return $this->_suite->runBlock($this, $closure, 'group');
324
    }
325
326
    /**
327
     * Group execution helper.
328
     */
329
    protected function _execute()
330
    {
331
        if (!$this->enabled() && !$this->focused()) {
332
            return;
333
        }
334
        foreach ($this->_children as $child) {
335 2
            if ($this->suite()->failfast()) {
336
                break;
337 1283
            }
338
            $this->_passed = $child->process() && $this->_passed;
339
        }
340
    }
341
342
    /**
343
     * Start group execution helper.
344
     */
345
    protected function _blockStart()
346
    {
347
        if (!$this->enabled()) {
348
            return;
349 452
        }
350 721
        $this->report('suiteStart', $this);
351
        $this->runCallbacks('beforeAll', false);
352
    }
353
354
    /**
355
     * End group block execution helper.
356
     */
357
    protected function _blockEnd($runAfterAll = true)
358
    {
359
        if (!$this->enabled()) {
360
            return;
361
        }
362
        if ($runAfterAll) {
363 831
            try {
364
                $this->runCallbacks('afterAll', false);
365 2
            } catch (Throwable $exception) {
366
                $this->_exception($exception);
367
            }
368
        }
369 833
370
        $this->suite()->autoclear();
371 833
372
        $type = $this->log()->type();
373 6
        if ($type === 'failed' || $type === 'errored') {
374 6
            $this->_passed = false;
375 6
            $this->suite()->failure();
376
            $this->summary()->log($this->log());
377
        }
378 833
379
        $this->report('suiteEnd', $this);
380
    }
381
382
    /**
383
     * Runs a callback.
384
     *
385
     * @param string $name The name of the callback (i.e `'beforeEach'` or `'afterEach'`).
386
     */
387
    public function runCallbacks($name, $recursive = true)
388 970
    {
389
        $instances = $recursive ? $this->parents(true) : [$this];
390 875
        if (strncmp($name, 'after', 5) === 0) {
391
            $instances = array_reverse($instances);
392
        }
393
        foreach ($instances as $instance) {
394 480
            foreach ($instance->_callbacks[$name] as $closure) {
395
                $this->_suite->runBlock($this, $closure, $name);
396
            }
397
        }
398
    }
399
400
    /**
401
     * Gets callbacks.
402
     *
403
     * @param  string $type The type of callbacks to get.
404
     *
405
     * @return array        The array callbacks instances.
406
     */
407
    public function callbacks($type)
408 8
    {
409
        return $this->_callbacks[$type] ?? [];
410
    }
411
412
    /**
413
     * Apply focus downward to the leaf.
414
     */
415
    public function broadcastFocus()
416
    {
417
        foreach ($this->_children as $child) {
418 2
            if ($child->type() !== 'normal') {
419
                continue;
420 6
            }
421
            $child->type('focus');
422 4
            if ($child instanceof Group) {
423
                $child->broadcastFocus();
424
            }
425
        }
426
    }
427
428
}
429