‪TYPO3CMS  10.4
splitFunctionalTests.php
Go to the documentation of this file.
1 #!/usr/bin/env php
2 <?php
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
18 use PhpParser\Comment\Doc;
19 use PhpParser\Node;
20 use PhpParser\NodeTraverser;
21 use PhpParser\NodeVisitor\NameResolver;
22 use PhpParser\NodeVisitorAbstract;
23 use PhpParser\ParserFactory;
24 use Symfony\Component\Console\Input\ArgvInput;
25 use Symfony\Component\Console\Input\InputArgument;
26 use Symfony\Component\Console\Input\InputDefinition;
27 use Symfony\Component\Console\Input\InputOption;
28 use Symfony\Component\Console\Output\ConsoleOutput;
29 use Symfony\Component\Console\Output\OutputInterface;
30 use Symfony\Component\Finder\Finder;
31 use Symfony\Component\Finder\SplFileInfo;
32 
33 if (PHP_SAPI !== 'cli') {
34  die('Script must be called from command line.' . chr(10));
35 }
36 
37 require __DIR__ . '/../../vendor/autoload.php';
38 
57 {
61  public function ‪execute()
62  {
63  $input = new ArgvInput($_SERVER['argv'], $this->‪getInputDefinition());
64  ‪$output = new ConsoleOutput();
65 
66  // Number of chunks and verbose output
67  $numberOfChunks = (int)$input->getArgument('numberOfChunks');
68 
69  if ($numberOfChunks < 1 || $numberOfChunks > 99) {
70  throw new \InvalidArgumentException(
71  'Main argument "numberOfChunks" must be at least 1 and maximum 99',
72  1528319388
73  );
74  }
75 
76  if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose', true)) {
77  ‪$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
78  }
79 
80  // Find functional test files
81  $testFiles = (new Finder())
82  ->files()
83  ->in(__DIR__ . '/../../typo3/sysext/*/Tests/Functional')
84  ->name('/Test\.php$/')
85  ->sortByName()
86  ;
87 
88  ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
89  $testStats = [];
90  foreach ($testFiles as $file) {
92  $relativeFilename = $file->getRealPath();
93  preg_match('/.*typo3\/sysext\/(.*)$/', $relativeFilename, $matches);
94  $relativeFilename = '../typo3/sysext/' . $matches[1];
95 
96  $ast = ‪$parser->parse($file->getContents());
97  $traverser = new NodeTraverser();
98  $visitor = new NameResolver();
99  $traverser->addVisitor($visitor);
100  $visitor = new ‪FunctionalTestCaseVisitor();
101  $traverser->addVisitor($visitor);
102  $traverser->traverse($ast);
103 
104  $fqcn = $visitor->getFqcn();
105  $tests = $visitor->getTests();
106  if (!empty($tests)) {
107  $testStats[$relativeFilename] = 0;
108  }
109 
110  foreach ($tests as $test) {
111  if (isset($test['dataProvider'])) {
112  // Test uses a data provider - get number of data sets
113  $dataProviderMethodName = $test['dataProvider'];
114  $methods = (new $fqcn())->$dataProviderMethodName();
115  if ($methods instanceof Generator) {
116  $numberOfDataSets = iterator_count($methods);
117  } else {
118  $numberOfDataSets = count($methods);
119  }
120  $testStats[$relativeFilename] += $numberOfDataSets;
121  } else {
122  // Just a single test
123  $testStats[$relativeFilename] += 1;
124  }
125  }
126  }
127 
128  // Sort test files by number of tests, descending
129  arsort($testStats);
130 
131  $this->‪createPhpunitXmlHeader($numberOfChunks);
132 
133  $numberOfTestsPerChunk = [];
134  for ($i = 1; $i <= $numberOfChunks; $i++) {
135  $numberOfTestsPerChunk[$i] = 0;
136  }
137 
138  foreach ($testStats as $testFile => $numberOfTestsInFile) {
139  // Sort list of tests per chunk by number of tests, pick lowest as
140  // the target of this test file
141  asort($numberOfTestsPerChunk);
142  reset($numberOfTestsPerChunk);
143  $jobFileNumber = key($numberOfTestsPerChunk);
144 
145  $content = <<<EOF
146  <directory>
147  $testFile
148  </directory>
149 
150 EOF;
151  file_put_contents(__DIR__ . '/../' . 'FunctionalTests-Job-' . $jobFileNumber . '.xml', $content, FILE_APPEND);
152 
153  $numberOfTestsPerChunk[$jobFileNumber] = $numberOfTestsPerChunk[$jobFileNumber] + $numberOfTestsInFile;
154  }
155 
156  $this->‪createPhpunitXmlFooter($numberOfChunks);
157 
158  if (‪$output->isVerbose()) {
159  ‪$output->writeln('Number of test files found: ' . count($testStats));
160  ‪$output->writeln('Number of tests found: ' . array_sum($testStats));
161  ‪$output->writeln('Number of chunks prepared: ' . $numberOfChunks);
162  ksort($numberOfTestsPerChunk);
163  foreach ($numberOfTestsPerChunk as $chunkNumber => $testNumber) {
164  ‪$output->writeln('Number of tests in chunk ' . $chunkNumber . ': ' . $testNumber);
165  }
166  }
167  }
168 
174  private function ‪getInputDefinition(): InputDefinition
175  {
176  return new InputDefinition([
177  new InputArgument('numberOfChunks', InputArgument::REQUIRED, 'Number of chunks / jobs to create'),
178  new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Enable verbose output'),
179  ]);
180  }
181 
187  private function ‪createPhpunitXmlHeader(int $numberOfChunks): void
188  {
189  $content = <<<EOF
190 <phpunit
191  backupGlobals="true"
192  bootstrap="../vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php"
193  colors="true"
194  convertErrorsToExceptions="true"
195  convertWarningsToExceptions="true"
196  forceCoversAnnotation="false"
197  stopOnError="false"
198  stopOnFailure="false"
199  stopOnIncomplete="false"
200  stopOnSkipped="false"
201  verbose="false"
202  beStrictAboutTestsThatDoNotTestAnything="false"
203 >
204  <testsuites>
205  <testsuite name="Core tests">
206 
207 EOF;
208  for ($i = 1; $i <= $numberOfChunks; $i++) {
209  file_put_contents(__DIR__ . '/../' . 'FunctionalTests-Job-' . $i . '.xml', $content);
210  }
211  }
212 
218  private function ‪createPhpunitXmlFooter(int $numberOfChunks): void
219  {
220  $content = <<<EOF
221  </testsuite>
222  </testsuites>
223 </phpunit>
224 
225 EOF;
226  for ($i = 1; $i <= $numberOfChunks; $i++) {
227  file_put_contents(__DIR__ . '/../' . 'FunctionalTests-Job-' . $i . '.xml', $content, FILE_APPEND);
228  }
229  }
230 }
231 
236 class ‪FunctionalTestCaseVisitor extends NodeVisitorAbstract
237 {
241  private ‪$tests = [];
242 
246  private ‪$fqcn;
247 
254  public function ‪enterNode(Node $node): void
255  {
256  if ($node instanceof Node\Stmt\Class_
257  && !$node->isAnonymous()
258  ) {
259  // The test class full namespace
260  $this->fqcn = (string)$node->namespacedName;
261  }
262 
263  if ($node instanceof Node\Stmt\ClassMethod
264  && ($docComment = $node->getDocComment()) instanceof Doc
265  ) {
266  preg_match_all(
267  '/\s*\s@(?<annotations>[^\s.].*)\n/',
268  $docComment->getText(),
269  $matches
270  );
271  foreach ($matches['annotations'] as $possibleTest) {
272  if ($possibleTest === 'test') {
273  // Found a test
274  $test = [
275  'methodName' => $node->name->name,
276  ];
277  foreach ($matches['annotations'] as $possibleDataProvider) {
278  // See if this test has a data provider attached
279  if (strpos($possibleDataProvider, 'dataProvider') === 0) {
280  $test['dataProvider'] = trim(ltrim($possibleDataProvider, 'dataProvider'));
281  }
282  }
283  $this->tests[] = $test;
284  }
285  }
286  }
287  }
288 
294  public function ‪getTests(): array
295  {
296  return ‪$this->tests;
297  }
298 
304  public function ‪getFqcn(): string
305  {
306  return ‪$this->fqcn;
307  }
308 }
309 
311 exit(‪$splitFunctionalTests->execute());
‪SplitFunctionalTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitFunctionalTests.php:174
‪FunctionalTestCaseVisitor\getFqcn
‪string getFqcn()
Definition: splitFunctionalTests.php:302
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitFunctionalTests.php:308
‪SplitFunctionalTests\execute
‪execute()
Definition: splitFunctionalTests.php:61
‪$parser
‪$parser
Definition: annotationChecker.php:108
‪SplitFunctionalTests\createPhpunitXmlFooter
‪createPhpunitXmlFooter(int $numberOfChunks)
Definition: splitFunctionalTests.php:218
‪FunctionalTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitFunctionalTests.php:252
‪SplitFunctionalTests\createPhpunitXmlHeader
‪createPhpunitXmlHeader(int $numberOfChunks)
Definition: splitFunctionalTests.php:187
‪FunctionalTestCaseVisitor\getTests
‪array getTests()
Definition: splitFunctionalTests.php:292
‪SplitFunctionalTests
Definition: splitFunctionalTests.php:57
‪FunctionalTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitFunctionalTests.php:244
‪$output
‪$output
Definition: annotationChecker.php:119
‪FunctionalTestCaseVisitor\$tests
‪array[] $tests
Definition: splitFunctionalTests.php:240
‪FunctionalTestCaseVisitor
Definition: splitFunctionalTests.php:237