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
|
|||
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
|
|||
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
|
|||
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 |
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 theid
property of an instance of theAccount
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.