3 * This file is part of PHPUnit.
5 * (c) Sebastian Bergmann <sebastian@phpunit.de>
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
11 if (!function_exists('trait_exists')) {
12 function trait_exists($traitname, $autoload = true)
21 * @since Class available since Release 3.0.0
23 class PHPUnit_Util_Test
25 const REGEX_DATA_PROVIDER = '/@dataProvider\s+([a-zA-Z0-9._:-\\\\x7f-\xff]+)/';
26 const REGEX_TEST_WITH = '/@testWith\s+/';
27 const REGEX_EXPECTED_EXCEPTION = '(@expectedException\s+([:.\w\\\\x7f-\xff]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m';
28 const REGEX_REQUIRES_VERSION = '/@requires\s+(?P<name>PHP(?:Unit)?)\s+(?P<value>[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m';
29 const REGEX_REQUIRES_OS = '/@requires\s+OS\s+(?P<value>.+?)[ \t]*\r?$/m';
30 const REGEX_REQUIRES = '/@requires\s+(?P<name>function|extension)\s+(?P<value>([^ ]+?))[ \t]*\r?$/m';
37 private static $annotationCache = array();
39 private static $hookMethods = array();
42 * @param PHPUnit_Framework_Test $test
43 * @param bool $asString
47 public static function describe(PHPUnit_Framework_Test $test, $asString = true)
50 if ($test instanceof PHPUnit_Framework_SelfDescribing) {
51 return $test->toString();
53 return get_class($test);
56 if ($test instanceof PHPUnit_Framework_TestCase) {
58 get_class($test), $test->getName()
60 } elseif ($test instanceof PHPUnit_Framework_SelfDescribing) {
61 return array('', $test->toString());
63 return array('', get_class($test));
69 * @param string $className
70 * @param string $methodName
74 * @throws PHPUnit_Framework_CodeCoverageException
76 * @since Method available since Release 4.0.0
78 public static function getLinesToBeCovered($className, $methodName)
80 $annotations = self::parseTestMethodAnnotations(
85 if (isset($annotations['class']['coversNothing']) || isset($annotations['method']['coversNothing'])) {
89 return self::getLinesToBeCoveredOrUsed($className, $methodName, 'covers');
93 * Returns lines of code specified with the @uses annotation.
95 * @param string $className
96 * @param string $methodName
100 * @since Method available since Release 4.0.0
102 public static function getLinesToBeUsed($className, $methodName)
104 return self::getLinesToBeCoveredOrUsed($className, $methodName, 'uses');
108 * @param string $className
109 * @param string $methodName
110 * @param string $mode
114 * @throws PHPUnit_Framework_CodeCoverageException
116 * @since Method available since Release 4.2.0
118 private static function getLinesToBeCoveredOrUsed($className, $methodName, $mode)
120 $annotations = self::parseTestMethodAnnotations(
125 $classShortcut = null;
127 if (!empty($annotations['class'][$mode . 'DefaultClass'])) {
128 if (count($annotations['class'][$mode . 'DefaultClass']) > 1) {
129 throw new PHPUnit_Framework_CodeCoverageException(
131 'More than one @%sClass annotation in class or interface "%s".',
138 $classShortcut = $annotations['class'][$mode . 'DefaultClass'][0];
143 if (isset($annotations['class'][$mode])) {
144 $list = $annotations['class'][$mode];
147 if (isset($annotations['method'][$mode])) {
148 $list = array_merge($list, $annotations['method'][$mode]);
153 foreach (array_unique($list) as $element) {
154 if ($classShortcut && strncmp($element, '::', 2) === 0) {
155 $element = $classShortcut . $element;
158 $element = preg_replace('/[\s()]+$/', '', $element);
159 $element = explode(' ', $element);
160 $element = $element[0];
162 $codeList = array_merge(
164 self::resolveElementToReflectionObjects($element)
168 return self::resolveReflectionObjectsToLines($codeList);
172 * Returns the requirements for a test.
174 * @param string $className
175 * @param string $methodName
179 * @since Method available since Release 3.6.0
181 public static function getRequirements($className, $methodName)
183 $reflector = new ReflectionClass($className);
184 $docComment = $reflector->getDocComment();
185 $reflector = new ReflectionMethod($className, $methodName);
186 $docComment .= "\n" . $reflector->getDocComment();
189 if ($count = preg_match_all(self::REGEX_REQUIRES_OS, $docComment, $matches)) {
190 $requires['OS'] = sprintf(
192 addcslashes($matches['value'][$count - 1], '/')
195 if ($count = preg_match_all(self::REGEX_REQUIRES_VERSION, $docComment, $matches)) {
196 for ($i = 0; $i < $count; $i++) {
197 $requires[$matches['name'][$i]] = $matches['value'][$i];
201 // https://bugs.php.net/bug.php?id=63055
204 if ($count = preg_match_all(self::REGEX_REQUIRES, $docComment, $matches)) {
205 for ($i = 0; $i < $count; $i++) {
206 $name = $matches['name'][$i] . 's';
207 if (!isset($requires[$name])) {
208 $requires[$name] = array();
210 $requires[$name][] = $matches['value'][$i];
218 * Returns the missing requirements for a test.
220 * @param string $className
221 * @param string $methodName
225 * @since Method available since Release 4.3.0
227 public static function getMissingRequirements($className, $methodName)
229 $required = static::getRequirements($className, $methodName);
232 if (!empty($required['PHP']) && version_compare(PHP_VERSION, $required['PHP'], '<')) {
233 $missing[] = sprintf('PHP %s (or later) is required.', $required['PHP']);
236 if (!empty($required['PHPUnit'])) {
237 $phpunitVersion = PHPUnit_Runner_Version::id();
238 if (version_compare($phpunitVersion, $required['PHPUnit'], '<')) {
239 $missing[] = sprintf('PHPUnit %s (or later) is required.', $required['PHPUnit']);
243 if (!empty($required['OS']) && !preg_match($required['OS'], PHP_OS)) {
244 $missing[] = sprintf('Operating system matching %s is required.', $required['OS']);
247 if (!empty($required['functions'])) {
248 foreach ($required['functions'] as $function) {
249 $pieces = explode('::', $function);
250 if (2 === count($pieces) && method_exists($pieces[0], $pieces[1])) {
253 if (function_exists($function)) {
256 $missing[] = sprintf('Function %s is required.', $function);
260 if (!empty($required['extensions'])) {
261 foreach ($required['extensions'] as $extension) {
262 if (!extension_loaded($extension)) {
263 $missing[] = sprintf('Extension %s is required.', $extension);
272 * Returns the expected exception for a test.
274 * @param string $className
275 * @param string $methodName
279 * @since Method available since Release 3.3.6
281 public static function getExpectedException($className, $methodName)
283 $reflector = new ReflectionMethod($className, $methodName);
284 $docComment = $reflector->getDocComment();
285 $docComment = substr($docComment, 3, -2);
287 if (preg_match(self::REGEX_EXPECTED_EXCEPTION, $docComment, $matches)) {
288 $annotations = self::parseTestMethodAnnotations(
293 $class = $matches[1];
298 if (isset($matches[2])) {
299 $message = trim($matches[2]);
300 } elseif (isset($annotations['method']['expectedExceptionMessage'])) {
301 $message = self::parseAnnotationContent(
302 $annotations['method']['expectedExceptionMessage'][0]
306 if (isset($annotations['method']['expectedExceptionMessageRegExp'])) {
307 $messageRegExp = self::parseAnnotationContent(
308 $annotations['method']['expectedExceptionMessageRegExp'][0]
312 if (isset($matches[3])) {
314 } elseif (isset($annotations['method']['expectedExceptionCode'])) {
315 $code = self::parseAnnotationContent(
316 $annotations['method']['expectedExceptionCode'][0]
320 if (is_numeric($code)) {
322 } elseif (is_string($code) && defined($code)) {
323 $code = (int) constant($code);
327 'class' => $class, 'code' => $code, 'message' => $message, 'message_regex' => $messageRegExp
335 * Parse annotation content to use constant/class constant values
337 * Constants are specified using a starting '@'. For example: @ClassName::CONST_NAME
339 * If the constant is not found the string is used as is to ensure maximum BC.
341 * @param string $message
345 private static function parseAnnotationContent($message)
347 if (strpos($message, '::') !== false && count(explode('::', $message)) == 2) {
348 if (defined($message)) {
349 $message = constant($message);
357 * Returns the provided data for a method.
359 * @param string $className
360 * @param string $methodName
362 * @return array|Iterator when a data provider is specified and exists
363 * null when no data provider is specified
365 * @throws PHPUnit_Framework_Exception
367 * @since Method available since Release 3.2.0
369 public static function getProvidedData($className, $methodName)
371 $reflector = new ReflectionMethod($className, $methodName);
372 $docComment = $reflector->getDocComment();
374 $data = self::getDataFromDataProviderAnnotation($docComment, $className, $methodName);
376 if ($data === null) {
377 $data = self::getDataFromTestWithAnnotation($docComment);
380 if (is_array($data) && empty($data)) {
381 throw new PHPUnit_Framework_SkippedTestError;
384 if ($data !== null) {
385 if (is_object($data)) {
386 $data = iterator_to_array($data);
389 foreach ($data as $key => $value) {
390 if (!is_array($value)) {
391 throw new PHPUnit_Framework_Exception(
393 'Data set %s is invalid.',
394 is_int($key) ? '#' . $key : '"' . $key . '"'
405 * Returns the provided data for a method.
407 * @param string $docComment
408 * @param string $className
409 * @param string $methodName
411 * @return array|Iterator when a data provider is specified and exists
412 * null when no data provider is specified
414 * @throws PHPUnit_Framework_Exception
416 private static function getDataFromDataProviderAnnotation($docComment, $className, $methodName)
418 if (preg_match(self::REGEX_DATA_PROVIDER, $docComment, $matches)) {
419 $dataProviderMethodNameNamespace = explode('\\', $matches[1]);
420 $leaf = explode('::', array_pop($dataProviderMethodNameNamespace));
421 $dataProviderMethodName = array_pop($leaf);
423 if (!empty($dataProviderMethodNameNamespace)) {
424 $dataProviderMethodNameNamespace = implode('\\', $dataProviderMethodNameNamespace) . '\\';
426 $dataProviderMethodNameNamespace = '';
430 $dataProviderClassName = $dataProviderMethodNameNamespace . array_pop($leaf);
432 $dataProviderClassName = $className;
435 $dataProviderClass = new ReflectionClass($dataProviderClassName);
436 $dataProviderMethod = $dataProviderClass->getMethod(
437 $dataProviderMethodName
440 if ($dataProviderMethod->isStatic()) {
443 $object = $dataProviderClass->newInstance();
446 if ($dataProviderMethod->getNumberOfParameters() == 0) {
447 $data = $dataProviderMethod->invoke($object);
449 $data = $dataProviderMethod->invoke($object, $methodName);
457 * @param string $docComment full docComment string
459 * @return array when @testWith annotation is defined
460 * null when @testWith annotation is omitted
462 * @throws PHPUnit_Framework_Exception when @testWith annotation is defined but cannot be parsed
464 public static function getDataFromTestWithAnnotation($docComment)
466 $docComment = self::cleanUpMultiLineAnnotation($docComment);
468 if (preg_match(self::REGEX_TEST_WITH, $docComment, $matches, PREG_OFFSET_CAPTURE)) {
469 $offset = strlen($matches[0][0]) + $matches[0][1];
470 $annotationContent = substr($docComment, $offset);
473 foreach (explode("\n", $annotationContent) as $candidateRow) {
474 $candidateRow = trim($candidateRow);
476 if ($candidateRow[0] !== '[') {
480 $dataSet = json_decode($candidateRow, true);
482 if (json_last_error() != JSON_ERROR_NONE) {
483 $error = function_exists('json_last_error_msg') ? json_last_error_msg() : json_last_error();
485 throw new PHPUnit_Framework_Exception(
486 'The dataset for the @testWith annotation cannot be parsed: ' . $error
494 throw new PHPUnit_Framework_Exception('The dataset for the @testWith annotation cannot be parsed.');
501 private static function cleanUpMultiLineAnnotation($docComment)
503 //removing initial ' * ' for docComment
504 $docComment = preg_replace('/' . '\n' . '\s*' . '\*' . '\s?' . '/', "\n", $docComment);
505 $docComment = substr($docComment, 0, -1);
506 $docComment = rtrim($docComment, "\n");
512 * @param string $className
513 * @param string $methodName
517 * @throws ReflectionException
519 * @since Method available since Release 3.4.0
521 public static function parseTestMethodAnnotations($className, $methodName = '')
523 if (!isset(self::$annotationCache[$className])) {
524 $class = new ReflectionClass($className);
525 self::$annotationCache[$className] = self::parseAnnotations($class->getDocComment());
528 if (!empty($methodName) && !isset(self::$annotationCache[$className . '::' . $methodName])) {
530 $method = new ReflectionMethod($className, $methodName);
531 $annotations = self::parseAnnotations($method->getDocComment());
532 } catch (ReflectionException $e) {
533 $annotations = array();
535 self::$annotationCache[$className . '::' . $methodName] = $annotations;
539 'class' => self::$annotationCache[$className],
540 'method' => !empty($methodName) ? self::$annotationCache[$className . '::' . $methodName] : array()
545 * @param string $docblock
549 * @since Method available since Release 3.4.0
551 private static function parseAnnotations($docblock)
553 $annotations = array();
554 // Strip away the docblock header and footer to ease parsing of one line annotations
555 $docblock = substr($docblock, 3, -2);
557 if (preg_match_all('/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m', $docblock, $matches)) {
558 $numMatches = count($matches[0]);
560 for ($i = 0; $i < $numMatches; ++$i) {
561 $annotations[$matches['name'][$i]][] = $matches['value'][$i];
569 * Returns the backup settings for a test.
571 * @param string $className
572 * @param string $methodName
576 * @since Method available since Release 3.4.0
578 public static function getBackupSettings($className, $methodName)
581 'backupGlobals' => self::getBooleanAnnotationSetting(
586 'backupStaticAttributes' => self::getBooleanAnnotationSetting(
589 'backupStaticAttributes'
595 * Returns the dependencies for a test class or method.
597 * @param string $className
598 * @param string $methodName
602 * @since Method available since Release 3.4.0
604 public static function getDependencies($className, $methodName)
606 $annotations = self::parseTestMethodAnnotations(
611 $dependencies = array();
613 if (isset($annotations['class']['depends'])) {
614 $dependencies = $annotations['class']['depends'];
617 if (isset($annotations['method']['depends'])) {
618 $dependencies = array_merge(
620 $annotations['method']['depends']
624 return array_unique($dependencies);
628 * Returns the error handler settings for a test.
630 * @param string $className
631 * @param string $methodName
635 * @since Method available since Release 3.4.0
637 public static function getErrorHandlerSettings($className, $methodName)
639 return self::getBooleanAnnotationSetting(
647 * Returns the groups for a test class or method.
649 * @param string $className
650 * @param string $methodName
654 * @since Method available since Release 3.2.0
656 public static function getGroups($className, $methodName = '')
658 $annotations = self::parseTestMethodAnnotations(
665 if (isset($annotations['method']['author'])) {
666 $groups = $annotations['method']['author'];
667 } elseif (isset($annotations['class']['author'])) {
668 $groups = $annotations['class']['author'];
671 if (isset($annotations['class']['group'])) {
672 $groups = array_merge($groups, $annotations['class']['group']);
675 if (isset($annotations['method']['group'])) {
676 $groups = array_merge($groups, $annotations['method']['group']);
679 if (isset($annotations['class']['ticket'])) {
680 $groups = array_merge($groups, $annotations['class']['ticket']);
683 if (isset($annotations['method']['ticket'])) {
684 $groups = array_merge($groups, $annotations['method']['ticket']);
687 foreach (array('method', 'class') as $element) {
688 foreach (array('small', 'medium', 'large') as $size) {
689 if (isset($annotations[$element][$size])) {
694 if (isset($annotations[$element][$size])) {
701 return array_unique($groups);
705 * Returns the size of the test.
707 * @param string $className
708 * @param string $methodName
712 * @since Method available since Release 3.6.0
714 public static function getSize($className, $methodName)
716 $groups = array_flip(self::getGroups($className, $methodName));
717 $size = self::UNKNOWN;
718 $class = new ReflectionClass($className);
720 if (isset($groups['large']) ||
721 (class_exists('PHPUnit_Extensions_Database_TestCase', false) &&
722 $class->isSubclassOf('PHPUnit_Extensions_Database_TestCase')) ||
723 (class_exists('PHPUnit_Extensions_SeleniumTestCase', false) &&
724 $class->isSubclassOf('PHPUnit_Extensions_SeleniumTestCase'))) {
726 } elseif (isset($groups['medium'])) {
727 $size = self::MEDIUM;
728 } elseif (isset($groups['small'])) {
736 * Returns the tickets for a test class or method.
738 * @param string $className
739 * @param string $methodName
743 * @since Method available since Release 3.4.0
745 public static function getTickets($className, $methodName)
747 $annotations = self::parseTestMethodAnnotations(
754 if (isset($annotations['class']['ticket'])) {
755 $tickets = $annotations['class']['ticket'];
758 if (isset($annotations['method']['ticket'])) {
759 $tickets = array_merge($tickets, $annotations['method']['ticket']);
762 return array_unique($tickets);
766 * Returns the process isolation settings for a test.
768 * @param string $className
769 * @param string $methodName
773 * @since Method available since Release 3.4.1
775 public static function getProcessIsolationSettings($className, $methodName)
777 $annotations = self::parseTestMethodAnnotations(
782 if (isset($annotations['class']['runTestsInSeparateProcesses']) ||
783 isset($annotations['method']['runInSeparateProcess'])) {
791 * Returns the preserve global state settings for a test.
793 * @param string $className
794 * @param string $methodName
798 * @since Method available since Release 3.4.0
800 public static function getPreserveGlobalStateSettings($className, $methodName)
802 return self::getBooleanAnnotationSetting(
805 'preserveGlobalState'
810 * @param string $className
814 * @since Method available since Release 4.0.8
816 public static function getHookMethods($className)
818 if (!class_exists($className, false)) {
819 return self::emptyHookMethodsArray();
822 if (!isset(self::$hookMethods[$className])) {
823 self::$hookMethods[$className] = self::emptyHookMethodsArray();
826 $class = new ReflectionClass($className);
828 foreach ($class->getMethods() as $method) {
829 if (self::isBeforeClassMethod($method)) {
830 self::$hookMethods[$className]['beforeClass'][] = $method->getName();
833 if (self::isBeforeMethod($method)) {
834 self::$hookMethods[$className]['before'][] = $method->getName();
837 if (self::isAfterMethod($method)) {
838 self::$hookMethods[$className]['after'][] = $method->getName();
841 if (self::isAfterClassMethod($method)) {
842 self::$hookMethods[$className]['afterClass'][] = $method->getName();
845 } catch (ReflectionException $e) {
849 return self::$hookMethods[$className];
855 * @since Method available since Release 4.0.9
857 private static function emptyHookMethodsArray()
860 'beforeClass' => array('setUpBeforeClass'),
861 'before' => array('setUp'),
862 'after' => array('tearDown'),
863 'afterClass' => array('tearDownAfterClass')
868 * @param string $className
869 * @param string $methodName
870 * @param string $settingName
874 * @since Method available since Release 3.4.0
876 private static function getBooleanAnnotationSetting($className, $methodName, $settingName)
878 $annotations = self::parseTestMethodAnnotations(
885 if (isset($annotations['class'][$settingName])) {
886 if ($annotations['class'][$settingName][0] == 'enabled') {
888 } elseif ($annotations['class'][$settingName][0] == 'disabled') {
893 if (isset($annotations['method'][$settingName])) {
894 if ($annotations['method'][$settingName][0] == 'enabled') {
896 } elseif ($annotations['method'][$settingName][0] == 'disabled') {
905 * @param string $element
909 * @throws PHPUnit_Framework_InvalidCoversTargetException
911 * @since Method available since Release 4.0.0
913 private static function resolveElementToReflectionObjects($element)
915 $codeToCoverList = array();
917 if (strpos($element, '\\') !== false && function_exists($element)) {
918 $codeToCoverList[] = new ReflectionFunction($element);
919 } elseif (strpos($element, '::') !== false) {
920 list($className, $methodName) = explode('::', $element);
922 if (isset($methodName[0]) && $methodName[0] == '<') {
923 $classes = array($className);
925 foreach ($classes as $className) {
926 if (!class_exists($className) &&
927 !interface_exists($className)) {
928 throw new PHPUnit_Framework_InvalidCoversTargetException(
930 'Trying to @cover or @use not existing class or ' .
937 $class = new ReflectionClass($className);
938 $methods = $class->getMethods();
939 $inverse = isset($methodName[1]) && $methodName[1] == '!';
941 if (strpos($methodName, 'protected')) {
942 $visibility = 'isProtected';
943 } elseif (strpos($methodName, 'private')) {
944 $visibility = 'isPrivate';
945 } elseif (strpos($methodName, 'public')) {
946 $visibility = 'isPublic';
949 foreach ($methods as $method) {
950 if ($inverse && !$method->$visibility()) {
951 $codeToCoverList[] = $method;
952 } elseif (!$inverse && $method->$visibility()) {
953 $codeToCoverList[] = $method;
958 $classes = array($className);
960 foreach ($classes as $className) {
961 if ($className == '' && function_exists($methodName)) {
962 $codeToCoverList[] = new ReflectionFunction(
966 if (!((class_exists($className) ||
967 interface_exists($className) ||
968 trait_exists($className)) &&
969 method_exists($className, $methodName))) {
970 throw new PHPUnit_Framework_InvalidCoversTargetException(
972 'Trying to @cover or @use not existing method "%s::%s".',
979 $codeToCoverList[] = new ReflectionMethod(
989 if (strpos($element, '<extended>') !== false) {
990 $element = str_replace('<extended>', '', $element);
994 $classes = array($element);
997 $classes = array_merge(
999 class_implements($element),
1000 class_parents($element)
1004 foreach ($classes as $className) {
1005 if (!class_exists($className) &&
1006 !interface_exists($className) &&
1007 !trait_exists($className)) {
1008 throw new PHPUnit_Framework_InvalidCoversTargetException(
1010 'Trying to @cover or @use not existing class or ' .
1017 $codeToCoverList[] = new ReflectionClass($className);
1021 return $codeToCoverList;
1025 * @param array $reflectors
1029 private static function resolveReflectionObjectsToLines(array $reflectors)
1033 foreach ($reflectors as $reflector) {
1034 $filename = $reflector->getFileName();
1036 if (!isset($result[$filename])) {
1037 $result[$filename] = array();
1040 $result[$filename] = array_unique(
1043 range($reflector->getStartLine(), $reflector->getEndLine())
1052 * @param ReflectionMethod $method
1056 * @since Method available since Release 4.0.8
1058 private static function isBeforeClassMethod(ReflectionMethod $method)
1060 return $method->isStatic() && strpos($method->getDocComment(), '@beforeClass') !== false;
1064 * @param ReflectionMethod $method
1068 * @since Method available since Release 4.0.8
1070 private static function isBeforeMethod(ReflectionMethod $method)
1072 return preg_match('/@before\b/', $method->getDocComment());
1076 * @param ReflectionMethod $method
1080 * @since Method available since Release 4.0.8
1082 private static function isAfterClassMethod(ReflectionMethod $method)
1084 return $method->isStatic() && strpos($method->getDocComment(), '@afterClass') !== false;
1088 * @param ReflectionMethod $method
1092 * @since Method available since Release 4.0.8
1094 private static function isAfterMethod(ReflectionMethod $method)
1096 return preg_match('/@after\b/', $method->getDocComment());