DuplicatedTestsTest.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Tests\AutoReview;
  13. use PhpCsFixer\Preg;
  14. use PhpCsFixer\Tests\TestCase;
  15. /**
  16. * @internal
  17. *
  18. * @coversNothing
  19. *
  20. * @group auto-review
  21. * @group covers-nothing
  22. */
  23. final class DuplicatedTestsTest extends TestCase
  24. {
  25. /**
  26. * @dataProvider \PhpCsFixer\Tests\AutoReview\ProjectCodeTest::provideTestClassCases
  27. *
  28. * @param class-string $className
  29. */
  30. public function testThatTestMethodsAreNotDuplicatedBasedOnContent(string $className): void
  31. {
  32. $alreadyFoundMethods = [];
  33. $duplicates = [];
  34. foreach (self::getMethodsForDuplicateCheck($className) as $method) {
  35. if (!str_starts_with($method->getName(), 'test')) {
  36. continue;
  37. }
  38. $startLine = (int) $method->getStartLine();
  39. $length = (int) $method->getEndLine() - $startLine;
  40. if (3 === $length) { // open and closing brace are included - this checks for single line methods
  41. continue;
  42. }
  43. /** @var list<string> $source */
  44. $source = file((string) $method->getFileName());
  45. $candidateContent = implode('', \array_slice($source, $startLine, $length));
  46. if (str_contains($candidateContent, '$this->doTest(')) {
  47. continue;
  48. }
  49. $foundInDuplicates = false;
  50. foreach ($alreadyFoundMethods as $methodKey => $methodContent) {
  51. if ($candidateContent === $methodContent) {
  52. $duplicates[] = \sprintf('%s is duplicate of %s', $methodKey, $method->getName());
  53. $foundInDuplicates = true;
  54. }
  55. }
  56. if (!$foundInDuplicates) {
  57. $alreadyFoundMethods[$method->getName()] = $candidateContent;
  58. }
  59. }
  60. self::assertSame(
  61. [],
  62. $duplicates,
  63. \sprintf(
  64. "Duplicated methods found in %s:\n - %s",
  65. $className,
  66. implode("\n - ", $duplicates)
  67. )
  68. );
  69. }
  70. /**
  71. * @dataProvider \PhpCsFixer\Tests\AutoReview\ProjectCodeTest::provideTestClassCases
  72. *
  73. * @param class-string $className
  74. */
  75. public function testThatTestMethodsAreNotDuplicatedBasedOnName(string $className): void
  76. {
  77. $alreadyFoundMethods = [];
  78. $duplicates = [];
  79. foreach (self::getMethodsForDuplicateCheck($className) as $method) {
  80. foreach ($alreadyFoundMethods as $alreadyFoundMethod) {
  81. if (!str_starts_with($method->getName(), $alreadyFoundMethod)) {
  82. continue;
  83. }
  84. $suffix = substr($method->getName(), \strlen($alreadyFoundMethod));
  85. if (!Preg::match('/^\d{2,}/', $suffix)) {
  86. continue;
  87. }
  88. $duplicates[] = \sprintf(
  89. 'Method "%s" must be shorter, call "%s".',
  90. $method->getName(),
  91. $alreadyFoundMethod
  92. );
  93. }
  94. $alreadyFoundMethods[] = $method->getName();
  95. }
  96. self::assertSame(
  97. [],
  98. $duplicates,
  99. \sprintf(
  100. "Duplicated methods found in %s:\n - %s",
  101. $className,
  102. implode("\n - ", $duplicates)
  103. )
  104. );
  105. }
  106. /**
  107. * @param class-string $className
  108. *
  109. * @return list<\ReflectionMethod>
  110. */
  111. private static function getMethodsForDuplicateCheck(string $className): array
  112. {
  113. static $methodsForDuplicateCheckCache = [];
  114. if (!isset($methodsForDuplicateCheckCache[$className])) {
  115. $class = new \ReflectionClass($className);
  116. $methodsForDuplicateCheck = array_filter(
  117. $class->getMethods(\ReflectionMethod::IS_PUBLIC),
  118. static fn (\ReflectionMethod $method) => str_starts_with($method->getName(), 'test')
  119. && $method->getDeclaringClass()->getName() === $className
  120. /*
  121. * Why 4?
  122. * Open and closing brace are included, this checks for:
  123. * - single line methods
  124. * - single line methods with configs
  125. */
  126. && 4 < (int) $method->getEndLine() - (int) $method->getStartLine()
  127. );
  128. usort(
  129. $methodsForDuplicateCheck,
  130. static fn (\ReflectionMethod $method1, \ReflectionMethod $method2) => $method1->getName() <=> $method2->getName(),
  131. );
  132. $methodsForDuplicateCheckCache[$className] = $methodsForDuplicateCheck;
  133. }
  134. return $methodsForDuplicateCheckCache[$className];
  135. }
  136. }