‪TYPO3CMS  11.5
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(): int
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  $numberOfTestsPerChunk = [];
132  $xml = [];
133  for ($i = 1; $i <= $numberOfChunks; $i++) {
134  $numberOfTestsPerChunk[$i] = 0;
135  // An xml parser per target file
136  $xml[$i] = new SimpleXMLElement(file_get_contents(__DIR__ . '/../phpunit/FunctionalTests.xml'));
137  // Drop existing directory spec
138  unset($xml[$i]->testsuites->testsuite->directory);
139  }
140 
141  foreach ($testStats as $testFile => $numberOfTestsInFile) {
142  // Sort list of tests per chunk by number of tests, pick lowest as
143  // the target of this test file
144  asort($numberOfTestsPerChunk);
145  reset($numberOfTestsPerChunk);
146  $jobFileNumber = key($numberOfTestsPerChunk);
147  $xml[$jobFileNumber]->testsuites->testsuite->addChild('directory', $testFile);
148  $numberOfTestsPerChunk[$jobFileNumber] = $numberOfTestsPerChunk[$jobFileNumber] + $numberOfTestsInFile;
149  }
150 
151  for ($i = 1; $i <= $numberOfChunks; $i++) {
152  // Write phpunit xml files
153  file_put_contents(__DIR__ . '/../phpunit/' . 'FunctionalTests-Job-' . $i . '.xml', $xml[$i]->asXml());
154  }
155 
156  if (‪$output->isVerbose()) {
157  ‪$output->writeln('Number of test files found: ' . count($testStats));
158  ‪$output->writeln('Number of tests found: ' . array_sum($testStats));
159  ‪$output->writeln('Number of chunks prepared: ' . $numberOfChunks);
160  ksort($numberOfTestsPerChunk);
161  foreach ($numberOfTestsPerChunk as $chunkNumber => $testNumber) {
162  ‪$output->writeln('Number of tests in chunk ' . $chunkNumber . ': ' . $testNumber);
163  }
164  }
165 
166  return 0;
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 }
182 
187 class ‪FunctionalTestCaseVisitor extends NodeVisitorAbstract
188 {
192  private ‪$tests = [];
193 
197  private ‪$fqcn;
198 
205  public function ‪enterNode(Node $node): void
206  {
207  if ($node instanceof Node\Stmt\Class_
208  && !$node->isAnonymous()
209  ) {
210  // The test class full namespace
211  $this->fqcn = (string)$node->namespacedName;
212  }
213 
214  if ($node instanceof Node\Stmt\ClassMethod
215  && ($docComment = $node->getDocComment()) instanceof Doc
216  ) {
217  preg_match_all(
218  '/\s*\s@(?<annotations>[^\s.].*)\n/',
219  $docComment->getText(),
220  $matches
221  );
222  foreach ($matches['annotations'] as $possibleTest) {
223  if ($possibleTest === 'test') {
224  // Found a test
225  $test = [
226  'methodName' => $node->name->name,
227  ];
228  foreach ($matches['annotations'] as $possibleDataProvider) {
229  // See if this test has a data provider attached
230  if (str_starts_with($possibleDataProvider, 'dataProvider')) {
231  $test['dataProvider'] = trim(ltrim($possibleDataProvider, 'dataProvider'));
232  }
233  }
234  $this->tests[] = $test;
235  }
236  }
237  }
238  }
239 
245  public function ‪getTests(): array
246  {
247  return ‪$this->tests;
248  }
249 
255  public function ‪getFqcn(): string
256  {
257  return ‪$this->fqcn;
258  }
259 }
260 
262 exit(‪$splitFunctionalTests->execute());
‪SplitFunctionalTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitFunctionalTests.php:174
‪FunctionalTestCaseVisitor\getFqcn
‪string getFqcn()
Definition: splitFunctionalTests.php:253
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitFunctionalTests.php:259
‪SplitFunctionalTests\execute
‪execute()
Definition: splitFunctionalTests.php:61
‪$parser
‪$parser
Definition: annotationChecker.php:110
‪FunctionalTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitFunctionalTests.php:203
‪FunctionalTestCaseVisitor\getTests
‪array getTests()
Definition: splitFunctionalTests.php:243
‪SplitFunctionalTests
Definition: splitFunctionalTests.php:57
‪FunctionalTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitFunctionalTests.php:195
‪$output
‪$output
Definition: annotationChecker.php:121
‪FunctionalTestCaseVisitor\$tests
‪array[] $tests
Definition: splitFunctionalTests.php:191
‪FunctionalTestCaseVisitor
Definition: splitFunctionalTests.php:188