1<?php
2/*
3 * This file is part of PHPUnit.
4 *
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10
11/**
12 * Test helpers.
13 */
14class PHPUnit_Util_Test
15{
16    const REGEX_DATA_PROVIDER      = '/@dataProvider\s+([a-zA-Z0-9._:-\\\\x7f-\xff]+)/';
17    const REGEX_TEST_WITH          = '/@testWith\s+/';
18    const REGEX_EXPECTED_EXCEPTION = '(@expectedException\s+([:.\w\\\\x7f-\xff]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m';
19    const REGEX_REQUIRES_VERSION   = '/@requires\s+(?P<name>PHP(?:Unit)?)\s+(?P<operator>[<>=!]{0,2})\s*(?P<version>[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m';
20    const REGEX_REQUIRES_OS        = '/@requires\s+OS\s+(?P<value>.+?)[ \t]*\r?$/m';
21    const REGEX_REQUIRES           = '/@requires\s+(?P<name>function|extension)\s+(?P<value>([^ ]+?))\s*(?P<operator>[<>=!]{0,2})\s*(?P<version>[\d\.-]+[\d\.]?)?[ \t]*\r?$/m';
22
23    const UNKNOWN = -1;
24    const SMALL   = 0;
25    const MEDIUM  = 1;
26    const LARGE   = 2;
27
28    private static $annotationCache = [];
29
30    private static $hookMethods = [];
31
32    /**
33     * @param PHPUnit_Framework_Test $test
34     * @param bool                   $asString
35     *
36     * @return mixed
37     */
38    public static function describe(PHPUnit_Framework_Test $test, $asString = true)
39    {
40        if ($asString) {
41            if ($test instanceof PHPUnit_Framework_SelfDescribing) {
42                return $test->toString();
43            } else {
44                return get_class($test);
45            }
46        } else {
47            if ($test instanceof PHPUnit_Framework_TestCase) {
48                return [
49                  get_class($test), $test->getName()
50                ];
51            } elseif ($test instanceof PHPUnit_Framework_SelfDescribing) {
52                return ['', $test->toString()];
53            } else {
54                return ['', get_class($test)];
55            }
56        }
57    }
58
59    /**
60     * @param string $className
61     * @param string $methodName
62     *
63     * @return array|bool
64     *
65     * @throws PHPUnit_Framework_CodeCoverageException
66     */
67    public static function getLinesToBeCovered($className, $methodName)
68    {
69        $annotations = self::parseTestMethodAnnotations(
70            $className,
71            $methodName
72        );
73
74        if (isset($annotations['class']['coversNothing']) || isset($annotations['method']['coversNothing'])) {
75            return false;
76        }
77
78        return self::getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
79    }
80
81    /**
82     * Returns lines of code specified with the @uses annotation.
83     *
84     * @param string $className
85     * @param string $methodName
86     *
87     * @return array
88     */
89    public static function getLinesToBeUsed($className, $methodName)
90    {
91        return self::getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
92    }
93
94    /**
95     * @param string $className
96     * @param string $methodName
97     * @param string $mode
98     *
99     * @return array
100     *
101     * @throws PHPUnit_Framework_CodeCoverageException
102     */
103    private static function getLinesToBeCoveredOrUsed($className, $methodName, $mode)
104    {
105        $annotations = self::parseTestMethodAnnotations(
106            $className,
107            $methodName
108        );
109
110        $classShortcut = null;
111
112        if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
113            if (count($annotations['class'][$mode . 'DefaultClass']) > 1) {
114                throw new PHPUnit_Framework_CodeCoverageException(
115                    sprintf(
116                        'More than one @%sClass annotation in class or interface "%s".',
117                        $mode,
118                        $className
119                    )
120                );
121            }
122
123            $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
124        }
125
126        $list = [];
127
128        if (isset($annotations['class'][$mode])) {
129            $list = $annotations['class'][$mode];
130        }
131
132        if (isset($annotations['method'][$mode])) {
133            $list = array_merge($list, $annotations['method'][$mode]);
134        }
135
136        $codeList = [];
137
138        foreach (array_unique($list) as $element) {
139            if ($classShortcut && strncmp($element, '::', 2) === 0) {
140                $element = $classShortcut . $element;
141            }
142
143            $element = preg_replace('/[\s()]+$/', '', $element);
144            $element = explode(' ', $element);
145            $element = $element[0];
146
147            $codeList = array_merge(
148                $codeList,
149                self::resolveElementToReflectionObjects($element)
150            );
151        }
152
153        return self::resolveReflectionObjectsToLines($codeList);
154    }
155
156    /**
157     * Returns the requirements for a test.
158     *
159     * @param string $className
160     * @param string $methodName
161     *
162     * @return array
163     */
164    public static function getRequirements($className, $methodName)
165    {
166        $reflector  = new ReflectionClass($className);
167        $docComment = $reflector->getDocComment();
168        $reflector  = new ReflectionMethod($className, $methodName);
169        $docComment .= "\n" . $reflector->getDocComment();
170        $requires   = [];
171
172        if ($count = preg_match_all(self::REGEX_REQUIRES_OS, $docComment, $matches)) {
173            $requires['OS'] = sprintf(
174                '/%s/i',
175                addcslashes($matches['value'][$count - 1], '/')
176            );
177        }
178        if ($count = preg_match_all(self::REGEX_REQUIRES_VERSION, $docComment, $matches)) {
179            for ($i = 0; $i < $count; $i++) {
180                $requires[$matches['name'][$i]] = [
181                    'version'  => $matches['version'][$i],
182                    'operator' => $matches['operator'][$i]
183                ];
184            }
185        }
186
187        // https://bugs.php.net/bug.php?id=63055
188        $matches = [];
189
190        if ($count = preg_match_all(self::REGEX_REQUIRES, $docComment, $matches)) {
191            for ($i = 0; $i < $count; $i++) {
192                $name = $matches['name'][$i] . 's';
193                if (!isset($requires[$name])) {
194                    $requires[$name] = [];
195                }
196                $requires[$name][] = $matches['value'][$i];
197                if (empty($matches['version'][$i]) || $name != 'extensions') {
198                    continue;
199                }
200                $requires['extension_versions'][$matches['value'][$i]] = [
201                    'version'  => $matches['version'][$i],
202                    'operator' => $matches['operator'][$i]
203                ];
204            }
205        }
206
207        return $requires;
208    }
209
210    /**
211     * Returns the missing requirements for a test.
212     *
213     * @param string $className
214     * @param string $methodName
215     *
216     * @return array
217     */
218    public static function getMissingRequirements($className, $methodName)
219    {
220        $required = static::getRequirements($className, $methodName);
221        $missing  = [];
222
223        $operator = empty($required['PHP']['operator']) ? '>=' : $required['PHP']['operator'];
224        if (!empty($required['PHP']) && !version_compare(PHP_VERSION, $required['PHP']['version'], $operator)) {
225            $missing[] = sprintf('PHP %s %s is required.', $operator, $required['PHP']['version']);
226        }
227
228        if (!empty($required['PHPUnit'])) {
229            $phpunitVersion = PHPUnit_Runner_Version::id();
230
231            $operator = empty($required['PHPUnit']['operator']) ? '>=' : $required['PHPUnit']['operator'];
232            if (!version_compare($phpunitVersion, $required['PHPUnit']['version'], $operator)) {
233                $missing[] = sprintf('PHPUnit %s %s is required.', $operator, $required['PHPUnit']['version']);
234            }
235        }
236
237        if (!empty($required['OS']) && !preg_match($required['OS'], PHP_OS)) {
238            $missing[] = sprintf('Operating system matching %s is required.', $required['OS']);
239        }
240
241        if (!empty($required['functions'])) {
242            foreach ($required['functions'] as $function) {
243                $pieces = explode('::', $function);
244                if (2 === count($pieces) && method_exists($pieces[0], $pieces[1])) {
245                    continue;
246                }
247                if (function_exists($function)) {
248                    continue;
249                }
250                $missing[] = sprintf('Function %s is required.', $function);
251            }
252        }
253
254        if (!empty($required['extensions'])) {
255            foreach ($required['extensions'] as $extension) {
256                if (isset($required['extension_versions'][$extension])) {
257                    continue;
258                }
259                if (!extension_loaded($extension)) {
260                    $missing[] = sprintf('Extension %s is required.', $extension);
261                }
262            }
263        }
264
265        if (!empty($required['extension_versions'])) {
266            foreach ($required['extension_versions'] as $extension => $required) {
267                $actualVersion = phpversion($extension);
268
269                $operator = empty($required['operator']) ? '>=' : $required['operator'];
270                if (false === $actualVersion || !version_compare($actualVersion, $required['version'], $operator)) {
271                    $missing[] = sprintf('Extension %s %s %s is required.', $extension, $operator, $required['version']);
272                }
273            }
274        }
275
276        return $missing;
277    }
278
279    /**
280     * Returns the expected exception for a test.
281     *
282     * @param string $className
283     * @param string $methodName
284     *
285     * @return array
286     */
287    public static function getExpectedException($className, $methodName)
288    {
289        $reflector  = new ReflectionMethod($className, $methodName);
290        $docComment = $reflector->getDocComment();
291        $docComment = substr($docComment, 3, -2);
292
293        if (preg_match(self::REGEX_EXPECTED_EXCEPTION, $docComment, $matches)) {
294            $annotations = self::parseTestMethodAnnotations(
295                $className,
296                $methodName
297            );
298
299            $class         = $matches[1];
300            $code          = null;
301            $message       = '';
302            $messageRegExp = '';
303
304            if (isset($matches[2])) {
305                $message = trim($matches[2]);
306            } elseif (isset($annotations['method']['expectedExceptionMessage'])) {
307                $message = self::parseAnnotationContent(
308                    $annotations['method']['expectedExceptionMessage'][0]
309                );
310            }
311
312            if (isset($annotations['method']['expectedExceptionMessageRegExp'])) {
313                $messageRegExp = self::parseAnnotationContent(
314                    $annotations['method']['expectedExceptionMessageRegExp'][0]
315                );
316            }
317
318            if (isset($matches[3])) {
319                $code = $matches[3];
320            } elseif (isset($annotations['method']['expectedExceptionCode'])) {
321                $code = self::parseAnnotationContent(
322                    $annotations['method']['expectedExceptionCode'][0]
323                );
324            }
325
326            if (is_numeric($code)) {
327                $code = (int) $code;
328            } elseif (is_string($code) && defined($code)) {
329                $code = (int) constant($code);
330            }
331
332            return [
333              'class' => $class, 'code' => $code, 'message' => $message, 'message_regex' => $messageRegExp
334            ];
335        }
336
337        return false;
338    }
339
340    /**
341     * Parse annotation content to use constant/class constant values
342     *
343     * Constants are specified using a starting '@'. For example: @ClassName::CONST_NAME
344     *
345     * If the constant is not found the string is used as is to ensure maximum BC.
346     *
347     * @param string $message
348     *
349     * @return string
350     */
351    private static function parseAnnotationContent($message)
352    {
353        if (strpos($message, '::') !== false && count(explode('::', $message)) == 2) {
354            if (defined($message)) {
355                $message = constant($message);
356            }
357        }
358
359        return $message;
360    }
361
362    /**
363     * Returns the provided data for a method.
364     *
365     * @param string $className
366     * @param string $methodName
367     *
368     * @return array When a data provider is specified and exists
369     *         null  When no data provider is specified
370     *
371     * @throws PHPUnit_Framework_Exception
372     */
373    public static function getProvidedData($className, $methodName)
374    {
375        $reflector  = new ReflectionMethod($className, $methodName);
376        $docComment = $reflector->getDocComment();
377
378        $data = self::getDataFromDataProviderAnnotation($docComment, $className, $methodName);
379
380        if ($data === null) {
381            $data = self::getDataFromTestWithAnnotation($docComment);
382        }
383
384        if (is_array($data) && empty($data)) {
385            throw new PHPUnit_Framework_SkippedTestError;
386        }
387
388        if ($data !== null) {
389            foreach ($data as $key => $value) {
390                if (!is_array($value)) {
391                    throw new PHPUnit_Framework_Exception(
392                        sprintf(
393                            'Data set %s is invalid.',
394                            is_int($key) ? '#' . $key : '"' . $key . '"'
395                        )
396                    );
397                }
398            }
399        }
400
401        return $data;
402    }
403
404    /**
405     * Returns the provided data for a method.
406     *
407     * @param string $docComment
408     * @param string $className
409     * @param string $methodName
410     *
411     * @return array|Iterator when a data provider is specified and exists
412     *                        null           when no data provider is specified
413     *
414     * @throws PHPUnit_Framework_Exception
415     */
416    private static function getDataFromDataProviderAnnotation($docComment, $className, $methodName)
417    {
418        if (preg_match_all(self::REGEX_DATA_PROVIDER, $docComment, $matches)) {
419            $result = [];
420
421            foreach ($matches[1] as $match) {
422                $dataProviderMethodNameNamespace = explode('\\', $match);
423                $leaf                            = explode('::', array_pop($dataProviderMethodNameNamespace));
424                $dataProviderMethodName          = array_pop($leaf);
425
426                if (!empty($dataProviderMethodNameNamespace)) {
427                    $dataProviderMethodNameNamespace = implode('\\', $dataProviderMethodNameNamespace) . '\\';
428                } else {
429                    $dataProviderMethodNameNamespace = '';
430                }
431
432                if (!empty($leaf)) {
433                    $dataProviderClassName = $dataProviderMethodNameNamespace . array_pop($leaf);
434                } else {
435                    $dataProviderClassName = $className;
436                }
437
438                $dataProviderClass  = new ReflectionClass($dataProviderClassName);
439                $dataProviderMethod = $dataProviderClass->getMethod(
440                    $dataProviderMethodName
441                );
442
443                if ($dataProviderMethod->isStatic()) {
444                    $object = null;
445                } else {
446                    $object = $dataProviderClass->newInstance();
447                }
448
449                if ($dataProviderMethod->getNumberOfParameters() == 0) {
450                    $data = $dataProviderMethod->invoke($object);
451                } else {
452                    $data = $dataProviderMethod->invoke($object, $methodName);
453                }
454
455                if ($data instanceof Iterator) {
456                    $data = iterator_to_array($data);
457                }
458
459                if (is_array($data)) {
460                    $result = array_merge($result, $data);
461                }
462            }
463
464            return $result;
465        }
466    }
467
468    /**
469     * @param string $docComment full docComment string
470     *
471     * @return array when @testWith annotation is defined
472     *               null  when @testWith annotation is omitted
473     *
474     * @throws PHPUnit_Framework_Exception when @testWith annotation is defined but cannot be parsed
475     */
476    public static function getDataFromTestWithAnnotation($docComment)
477    {
478        $docComment = self::cleanUpMultiLineAnnotation($docComment);
479
480        if (preg_match(self::REGEX_TEST_WITH, $docComment, $matches, PREG_OFFSET_CAPTURE)) {
481            $offset            = strlen($matches[0][0]) + $matches[0][1];
482            $annotationContent = substr($docComment, $offset);
483            $data              = [];
484
485            foreach (explode("\n", $annotationContent) as $candidateRow) {
486                $candidateRow = trim($candidateRow);
487
488                if ($candidateRow[0] !== '[') {
489                    break;
490                }
491
492                $dataSet = json_decode($candidateRow, true);
493
494                if (json_last_error() != JSON_ERROR_NONE) {
495                    throw new PHPUnit_Framework_Exception(
496                        'The dataset for the @testWith annotation cannot be parsed: ' . json_last_error_msg()
497                    );
498                }
499
500                $data[] = $dataSet;
501            }
502
503            if (!$data) {
504                throw new PHPUnit_Framework_Exception('The dataset for the @testWith annotation cannot be parsed.');
505            }
506
507            return $data;
508        }
509    }
510
511    private static function cleanUpMultiLineAnnotation($docComment)
512    {
513        //removing initial '   * ' for docComment
514        $docComment = str_replace("\r\n", "\n", $docComment);
515        $docComment = preg_replace('/' . '\n' . '\s*' . '\*' . '\s?' . '/', "\n", $docComment);
516        $docComment = substr($docComment, 0, -1);
517        $docComment = rtrim($docComment, "\n");
518
519        return $docComment;
520    }
521
522    /**
523     * @param string $className
524     * @param string $methodName
525     *
526     * @return array
527     *
528     * @throws ReflectionException
529     */
530    public static function parseTestMethodAnnotations($className, $methodName = '')
531    {
532        if (!isset(self::$annotationCache[$className])) {
533            $class                             = new ReflectionClass($className);
534            self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment());
535        }
536
537        if (!empty($methodName) && !isset(self::$annotationCache[$className . '::' . $methodName])) {
538            try {
539                $method      = new ReflectionMethod($className, $methodName);
540                $annotations = self::parseAnnotations($method->getDocComment());
541            } catch (ReflectionException $e) {
542                $annotations = [];
543            }
544            self::$annotationCache[$className . '::' . $methodName] = $annotations;
545        }
546
547        return [
548          'class'  => self::$annotationCache[$className],
549          'method' => !empty($methodName) ? self::$annotationCache[$className . '::' . $methodName] : []
550        ];
551    }
552
553    /**
554     * @param string $className
555     * @param string $methodName
556     *
557     * @return array
558     */
559    public static function getInlineAnnotations($className, $methodName)
560    {
561        $method      = new ReflectionMethod($className, $methodName);
562        $code        = file($method->getFileName());
563        $lineNumber  = $method->getStartLine();
564        $startLine   = $method->getStartLine() - 1;
565        $endLine     = $method->getEndLine() - 1;
566        $methodLines = array_slice($code, $startLine, $endLine - $startLine + 1);
567        $annotations = [];
568
569        foreach ($methodLines as $line) {
570            if (preg_match('#/\*\*?\s*@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?\*/$#m', $line, $matches)) {
571                $annotations[strtolower($matches['name'])] = [
572                    'line'  => $lineNumber,
573                    'value' => $matches['value']
574                ];
575            }
576
577            $lineNumber++;
578        }
579
580        return $annotations;
581    }
582
583    /**
584     * @param string $docblock
585     *
586     * @return array
587     */
588    private static function parseAnnotations($docblock)
589    {
590        $annotations = [];
591        // Strip away the docblock header and footer to ease parsing of one line annotations
592        $docblock = substr($docblock, 3, -2);
593
594        if (preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docblock, $matches)) {
595            $numMatches = count($matches[0]);
596
597            for ($i = 0; $i < $numMatches; ++$i) {
598                $annotations[$matches['name'][$i]][] = (string) $matches['value'][$i];
599            }
600        }
601
602        return $annotations;
603    }
604
605    /**
606     * Returns the backup settings for a test.
607     *
608     * @param string $className
609     * @param string $methodName
610     *
611     * @return array
612     */
613    public static function getBackupSettings($className, $methodName)
614    {
615        return [
616          'backupGlobals' => self::getBooleanAnnotationSetting(
617              $className,
618              $methodName,
619              'backupGlobals'
620          ),
621          'backupStaticAttributes' => self::getBooleanAnnotationSetting(
622              $className,
623              $methodName,
624              'backupStaticAttributes'
625          )
626        ];
627    }
628
629    /**
630     * Returns the dependencies for a test class or method.
631     *
632     * @param string $className
633     * @param string $methodName
634     *
635     * @return array
636     */
637    public static function getDependencies($className, $methodName)
638    {
639        $annotations = self::parseTestMethodAnnotations(
640            $className,
641            $methodName
642        );
643
644        $dependencies = [];
645
646        if (isset($annotations['class']['depends'])) {
647            $dependencies = $annotations['class']['depends'];
648        }
649
650        if (isset($annotations['method']['depends'])) {
651            $dependencies = array_merge(
652                $dependencies,
653                $annotations['method']['depends']
654            );
655        }
656
657        return array_unique($dependencies);
658    }
659
660    /**
661     * Returns the error handler settings for a test.
662     *
663     * @param string $className
664     * @param string $methodName
665     *
666     * @return bool
667     */
668    public static function getErrorHandlerSettings($className, $methodName)
669    {
670        return self::getBooleanAnnotationSetting(
671            $className,
672            $methodName,
673            'errorHandler'
674        );
675    }
676
677    /**
678     * Returns the groups for a test class or method.
679     *
680     * @param string $className
681     * @param string $methodName
682     *
683     * @return array
684     */
685    public static function getGroups($className, $methodName = '')
686    {
687        $annotations = self::parseTestMethodAnnotations(
688            $className,
689            $methodName
690        );
691
692        $groups = [];
693
694        if (isset($annotations['method']['author'])) {
695            $groups = $annotations['method']['author'];
696        } elseif (isset($annotations['class']['author'])) {
697            $groups = $annotations['class']['author'];
698        }
699
700        if (isset($annotations['class']['group'])) {
701            $groups = array_merge($groups, $annotations['class']['group']);
702        }
703
704        if (isset($annotations['method']['group'])) {
705            $groups = array_merge($groups, $annotations['method']['group']);
706        }
707
708        if (isset($annotations['class']['ticket'])) {
709            $groups = array_merge($groups, $annotations['class']['ticket']);
710        }
711
712        if (isset($annotations['method']['ticket'])) {
713            $groups = array_merge($groups, $annotations['method']['ticket']);
714        }
715
716        foreach (['method', 'class'] as $element) {
717            foreach (['small', 'medium', 'large'] as $size) {
718                if (isset($annotations[$element][$size])) {
719                    $groups[] = $size;
720                    break 2;
721                }
722            }
723        }
724
725        return array_unique($groups);
726    }
727
728    /**
729     * Returns the size of the test.
730     *
731     * @param string $className
732     * @param string $methodName
733     *
734     * @return int
735     */
736    public static function getSize($className, $methodName)
737    {
738        $groups = array_flip(self::getGroups($className, $methodName));
739        $size   = self::UNKNOWN;
740        $class  = new ReflectionClass($className);
741
742        if (isset($groups['large']) ||
743            (class_exists('PHPUnit_Extensions_Database_TestCase', false) &&
744             $class->isSubclassOf('PHPUnit_Extensions_Database_TestCase'))) {
745            $size = self::LARGE;
746        } elseif (isset($groups['medium'])) {
747            $size = self::MEDIUM;
748        } elseif (isset($groups['small'])) {
749            $size = self::SMALL;
750        }
751
752        return $size;
753    }
754
755    /**
756     * Returns the tickets for a test class or method.
757     *
758     * @param string $className
759     * @param string $methodName
760     *
761     * @return array
762     */
763    public static function getTickets($className, $methodName)
764    {
765        $annotations = self::parseTestMethodAnnotations(
766            $className,
767            $methodName
768        );
769
770        $tickets = [];
771
772        if (isset($annotations['class']['ticket'])) {
773            $tickets = $annotations['class']['ticket'];
774        }
775
776        if (isset($annotations['method']['ticket'])) {
777            $tickets = array_merge($tickets, $annotations['method']['ticket']);
778        }
779
780        return array_unique($tickets);
781    }
782
783    /**
784     * Returns the process isolation settings for a test.
785     *
786     * @param string $className
787     * @param string $methodName
788     *
789     * @return bool
790     */
791    public static function getProcessIsolationSettings($className, $methodName)
792    {
793        $annotations = self::parseTestMethodAnnotations(
794            $className,
795            $methodName
796        );
797
798        if (isset($annotations['class']['runTestsInSeparateProcesses']) ||
799            isset($annotations['method']['runInSeparateProcess'])) {
800            return true;
801        } else {
802            return false;
803        }
804    }
805
806    /**
807     * Returns the preserve global state settings for a test.
808     *
809     * @param string $className
810     * @param string $methodName
811     *
812     * @return bool
813     */
814    public static function getPreserveGlobalStateSettings($className, $methodName)
815    {
816        return self::getBooleanAnnotationSetting(
817            $className,
818            $methodName,
819            'preserveGlobalState'
820        );
821    }
822
823    /**
824     * @param string $className
825     *
826     * @return array
827     */
828    public static function getHookMethods($className)
829    {
830        if (!class_exists($className, false)) {
831            return self::emptyHookMethodsArray();
832        }
833
834        if (!isset(self::$hookMethods[$className])) {
835            self::$hookMethods[$className] = self::emptyHookMethodsArray();
836
837            try {
838                $class = new ReflectionClass($className);
839
840                foreach ($class->getMethods() as $method) {
841                    if (self::isBeforeClassMethod($method)) {
842                        self::$hookMethods[$className]['beforeClass'][] = $method->getName();
843                    }
844
845                    if (self::isBeforeMethod($method)) {
846                        self::$hookMethods[$className]['before'][] = $method->getName();
847                    }
848
849                    if (self::isAfterMethod($method)) {
850                        self::$hookMethods[$className]['after'][] = $method->getName();
851                    }
852
853                    if (self::isAfterClassMethod($method)) {
854                        self::$hookMethods[$className]['afterClass'][] = $method->getName();
855                    }
856                }
857            } catch (ReflectionException $e) {
858            }
859        }
860
861        return self::$hookMethods[$className];
862    }
863
864    /**
865     * @return array
866     */
867    private static function emptyHookMethodsArray()
868    {
869        return [
870            'beforeClass' => ['setUpBeforeClass'],
871            'before'      => ['setUp'],
872            'after'       => ['tearDown'],
873            'afterClass'  => ['tearDownAfterClass']
874        ];
875    }
876
877    /**
878     * @param string $className
879     * @param string $methodName
880     * @param string $settingName
881     *
882     * @return bool
883     */
884    private static function getBooleanAnnotationSetting($className, $methodName, $settingName)
885    {
886        $annotations = self::parseTestMethodAnnotations(
887            $className,
888            $methodName
889        );
890
891        $result = null;
892
893        if (isset($annotations['class'][$settingName])) {
894            if ($annotations['class'][$settingName][0] == 'enabled') {
895                $result = true;
896            } elseif ($annotations['class'][$settingName][0] == 'disabled') {
897                $result = false;
898            }
899        }
900
901        if (isset($annotations['method'][$settingName])) {
902            if ($annotations['method'][$settingName][0] == 'enabled') {
903                $result = true;
904            } elseif ($annotations['method'][$settingName][0] == 'disabled') {
905                $result = false;
906            }
907        }
908
909        return $result;
910    }
911
912    /**
913     * @param string $element
914     *
915     * @return array
916     *
917     * @throws PHPUnit_Framework_InvalidCoversTargetException
918     */
919    private static function resolveElementToReflectionObjects($element)
920    {
921        $codeToCoverList = [];
922
923        if (strpos($element, '\\') !== false && function_exists($element)) {
924            $codeToCoverList[] = new ReflectionFunction($element);
925        } elseif (strpos($element, '::') !== false) {
926            list($className, $methodName) = explode('::', $element);
927
928            if (isset($methodName[0]) && $methodName[0] == '<') {
929                $classes = [$className];
930
931                foreach ($classes as $className) {
932                    if (!class_exists($className) &&
933                        !interface_exists($className) &&
934                        !trait_exists($className)) {
935                        throw new PHPUnit_Framework_InvalidCoversTargetException(
936                            sprintf(
937                                'Trying to @cover or @use not existing class or ' .
938                                'interface "%s".',
939                                $className
940                            )
941                        );
942                    }
943
944                    $class   = new ReflectionClass($className);
945                    $methods = $class->getMethods();
946                    $inverse = isset($methodName[1]) && $methodName[1] == '!';
947
948                    if (strpos($methodName, 'protected')) {
949                        $visibility = 'isProtected';
950                    } elseif (strpos($methodName, 'private')) {
951                        $visibility = 'isPrivate';
952                    } elseif (strpos($methodName, 'public')) {
953                        $visibility = 'isPublic';
954                    }
955
956                    foreach ($methods as $method) {
957                        if ($inverse && !$method->$visibility()) {
958                            $codeToCoverList[] = $method;
959                        } elseif (!$inverse && $method->$visibility()) {
960                            $codeToCoverList[] = $method;
961                        }
962                    }
963                }
964            } else {
965                $classes = [$className];
966
967                foreach ($classes as $className) {
968                    if ($className == '' && function_exists($methodName)) {
969                        $codeToCoverList[] = new ReflectionFunction(
970                            $methodName
971                        );
972                    } else {
973                        if (!((class_exists($className) ||
974                               interface_exists($className) ||
975                               trait_exists($className)) &&
976                              method_exists($className, $methodName))) {
977                            throw new PHPUnit_Framework_InvalidCoversTargetException(
978                                sprintf(
979                                    'Trying to @cover or @use not existing method "%s::%s".',
980                                    $className,
981                                    $methodName
982                                )
983                            );
984                        }
985
986                        $codeToCoverList[] = new ReflectionMethod(
987                            $className,
988                            $methodName
989                        );
990                    }
991                }
992            }
993        } else {
994            $extended = false;
995
996            if (strpos($element, '<extended>') !== false) {
997                $element  = str_replace('<extended>', '', $element);
998                $extended = true;
999            }
1000
1001            $classes = [$element];
1002
1003            if ($extended) {
1004                $classes = array_merge(
1005                    $classes,
1006                    class_implements($element),
1007                    class_parents($element)
1008                );
1009            }
1010
1011            foreach ($classes as $className) {
1012                if (!class_exists($className) &&
1013                    !interface_exists($className) &&
1014                    !trait_exists($className)) {
1015                    throw new PHPUnit_Framework_InvalidCoversTargetException(
1016                        sprintf(
1017                            'Trying to @cover or @use not existing class or ' .
1018                            'interface "%s".',
1019                            $className
1020                        )
1021                    );
1022                }
1023
1024                $codeToCoverList[] = new ReflectionClass($className);
1025            }
1026        }
1027
1028        return $codeToCoverList;
1029    }
1030
1031    /**
1032     * @param array $reflectors
1033     *
1034     * @return array
1035     */
1036    private static function resolveReflectionObjectsToLines(array $reflectors)
1037    {
1038        $result = [];
1039
1040        foreach ($reflectors as $reflector) {
1041            $filename = $reflector->getFileName();
1042
1043            if (!isset($result[$filename])) {
1044                $result[$filename] = [];
1045            }
1046
1047            $result[$filename] = array_merge(
1048                $result[$filename],
1049                range($reflector->getStartLine(), $reflector->getEndLine())
1050            );
1051        }
1052
1053        foreach ($result as $filename => $lineNumbers) {
1054            $result[$filename] = array_keys(array_flip($lineNumbers));
1055        }
1056
1057        return $result;
1058    }
1059
1060    /**
1061     * @param ReflectionMethod $method
1062     *
1063     * @return bool
1064     */
1065    private static function isBeforeClassMethod(ReflectionMethod $method)
1066    {
1067        return $method->isStatic() && strpos($method->getDocComment(), '@beforeClass') !== false;
1068    }
1069
1070    /**
1071     * @param ReflectionMethod $method
1072     *
1073     * @return bool
1074     */
1075    private static function isBeforeMethod(ReflectionMethod $method)
1076    {
1077        return preg_match('/@before\b/', $method->getDocComment());
1078    }
1079
1080    /**
1081     * @param ReflectionMethod $method
1082     *
1083     * @return bool
1084     */
1085    private static function isAfterClassMethod(ReflectionMethod $method)
1086    {
1087        return $method->isStatic() && strpos($method->getDocComment(), '@afterClass') !== false;
1088    }
1089
1090    /**
1091     * @param ReflectionMethod $method
1092     *
1093     * @return bool
1094     */
1095    private static function isAfterMethod(ReflectionMethod $method)
1096    {
1097        return preg_match('/@after\b/', $method->getDocComment());
1098    }
1099}
1100