‪TYPO3CMS  ‪main
splitFunctionalTests.php
Go to the documentation of this file.
1 #!/usr/bin/env php
2 <?php
3 
4 declare(strict_types=1);
5 
6 /*
7  * This file is part of the TYPO3 CMS project.
8  *
9  * It is free software; you can redistribute it and/or modify it under
10  * the terms of the GNU General Public License, either version 2
11  * of the License, or any later version.
12  *
13  * For the full copyright and license information, please read the
14  * LICENSE.txt file that was distributed with this source code.
15  *
16  * The TYPO3 project - inspiring people to share!
17  */
18 
19 use PhpParser\Node;
20 use PhpParser\NodeTraverser;
21 use PhpParser\NodeVisitor\NameResolver;
22 use PhpParser\NodeVisitorAbstract;
23 use PhpParser\ParserFactory;
24 use PhpParser\PhpVersion;
25 use Symfony\Component\Console\Input\ArgvInput;
26 use Symfony\Component\Console\Input\InputArgument;
27 use Symfony\Component\Console\Input\InputDefinition;
28 use Symfony\Component\Console\Input\InputOption;
29 use Symfony\Component\Console\Output\ConsoleOutput;
30 use Symfony\Component\Console\Output\OutputInterface;
31 use Symfony\Component\Finder\Finder;
32 use Symfony\Component\Finder\SplFileInfo;
33 
34 if (PHP_SAPI !== 'cli') {
35  die('Script must be called from command line.' . chr(10));
36 }
37 
38 require __DIR__ . '/../../vendor/autoload.php';
39 
58 {
62  public function ‪execute(): int
63  {
64  $input = new ArgvInput(‪$_SERVER['argv'], $this->‪getInputDefinition());
65  ‪$output = new ConsoleOutput();
66 
67  // Number of chunks and verbose output
68  $numberOfChunks = (int)$input->getArgument('numberOfChunks');
69 
70  if ($numberOfChunks < 1 || $numberOfChunks > 99) {
71  throw new \InvalidArgumentException(
72  'Main argument "numberOfChunks" must be at least 1 and maximum 99',
73  1528319388
74  );
75  }
76 
77  if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose', true)) {
78  ‪$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
79  }
80 
81  // Find functional test files
82  $testFiles = (new Finder())
83  ->files()
84  ->in(__DIR__ . '/../../typo3/sysext/*/Tests/Functional')
85  ->name('/Test\.php$/')
86  ->sortByName()
87  ;
88 
89  ‪$parser = (new ParserFactory())->createForVersion(PhpVersion::fromComponents(8, 2));
90  $testStats = [];
91  foreach ($testFiles as $file) {
93  $relativeFilename = $file->getRealPath();
94  preg_match('/.*typo3\/sysext\/(.*)$/', $relativeFilename, $matches);
95  $relativeFilename = '../../typo3/sysext/' . $matches[1];
96 
97  $ast = ‪$parser->parse($file->getContents());
98  $traverser = new NodeTraverser();
99  $visitor = new NameResolver();
100  $traverser->addVisitor($visitor);
101  $visitor = new ‪FunctionalTestCaseVisitor();
102  $traverser->addVisitor($visitor);
103  $traverser->traverse($ast);
104 
105  $fqcn = $visitor->getFqcn();
106  $tests = $visitor->getTests();
107  if (!empty($tests)) {
108  $testStats[$relativeFilename] = 0;
109  }
110 
111  foreach ($tests as $test) {
112  if (isset($test['dataProvider'])) {
113  // Test uses a data provider - get number of data sets
114  $dataProviderMethodName = $test['dataProvider'];
115  $methods = $fqcn::$dataProviderMethodName();
116  if ($methods instanceof Generator) {
117  $numberOfDataSets = iterator_count($methods);
118  } else {
119  $numberOfDataSets = count($methods);
120  }
121  $testStats[$relativeFilename] += $numberOfDataSets;
122  } else {
123  // Just a single test
124  $testStats[$relativeFilename] += 1;
125  }
126  }
127  }
128 
129  // Sort test files by number of tests, descending
130  arsort($testStats);
131 
132  $numberOfTestsPerChunk = [];
133  $xml = [];
134  for ($i = 1; $i <= $numberOfChunks; $i++) {
135  $numberOfTestsPerChunk[$i] = 0;
136  // An xml parser per target file
137  $xml[$i] = new SimpleXMLElement(file_get_contents(__DIR__ . '/../phpunit/FunctionalTests.xml'));
138  // Drop existing directory spec
139  unset($xml[$i]->testsuites->testsuite->directory);
140  }
141 
142  foreach ($testStats as $testFile => $numberOfTestsInFile) {
143  // Sort list of tests per chunk by number of tests, pick lowest as
144  // the target of this test file
145  asort($numberOfTestsPerChunk);
146  reset($numberOfTestsPerChunk);
147  $jobFileNumber = key($numberOfTestsPerChunk);
148  $xml[$jobFileNumber]->testsuites->testsuite->addChild('file', $testFile);
149  $numberOfTestsPerChunk[$jobFileNumber] = $numberOfTestsPerChunk[$jobFileNumber] + $numberOfTestsInFile;
150  }
151 
152  for ($i = 1; $i <= $numberOfChunks; $i++) {
153  // Write phpunit xml files
154  file_put_contents(__DIR__ . '/../phpunit/' . 'FunctionalTests-Job-' . $i . '.xml', $xml[$i]->asXml());
155  }
156 
157  if (‪$output->isVerbose()) {
158  ‪$output->writeln('Number of test files found: ' . count($testStats));
159  ‪$output->writeln('Number of tests found: ' . array_sum($testStats));
160  ‪$output->writeln('Number of chunks prepared: ' . $numberOfChunks);
161  ksort($numberOfTestsPerChunk);
162  foreach ($numberOfTestsPerChunk as $chunkNumber => $testNumber) {
163  ‪$output->writeln('Number of tests in chunk ' . $chunkNumber . ': ' . $testNumber);
164  }
165  }
166 
167  return 0;
168  }
169 
175  private function ‪getInputDefinition(): InputDefinition
176  {
177  return new InputDefinition([
178  new InputArgument('numberOfChunks', InputArgument::REQUIRED, 'Number of chunks / jobs to create'),
179  new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Enable verbose output'),
180  ]);
181  }
182 }
183 
188 class ‪FunctionalTestCaseVisitor extends NodeVisitorAbstract
189 {
193  private array ‪$tests = [];
194 
198  private string ‪$fqcn;
199 
204  public function ‪enterNode(Node $node): void
205  {
206  if ($node instanceof Node\Stmt\Class_
207  && !$node->isAnonymous()
208  ) {
209  // The test class full namespace
210  $this->fqcn = (string)$node->namespacedName;
211  }
212 
213  if ($node instanceof Node\Stmt\ClassMethod) {
214  foreach ($node->getAttrGroups() as $possibleTestAttributeGroup) {
215  foreach ($possibleTestAttributeGroup->attrs as $possibleTestAttribute) {
216  // See if that method has the phpunit Test attribute attached.
217  $name = $possibleTestAttribute->name->toCodeString();
218  if ($name === '\\PHPUnit\\Framework\\Attributes\\Test') {
219  $test = [
220  'methodName' => $node->name->name,
221  ];
222  foreach ($node->getAttrGroups() as $possibleDataProviderAttributeGroup) {
223  foreach ($possibleDataProviderAttributeGroup->attrs as $possibleDataProviderAttribute) {
224  // See if that method has the phpunit DataProvider attribute attached, too.
225  $name = $possibleDataProviderAttribute->name->toCodeString();
226  if ($name === '\\PHPUnit\\Framework\\Attributes\\DataProvider') {
227  $dataProviderMethodName = $possibleDataProviderAttribute->args[0]->value->value;
228  $test['dataProvider'] = $dataProviderMethodName;
229  }
230  }
231  }
232  $this->tests[] = $test;
233  }
234  }
235  }
236  }
237  }
238 
242  public function ‪getTests(): array
243  {
244  return ‪$this->tests;
245  }
246 
250  public function ‪getFqcn(): string
251  {
252  return ‪$this->fqcn;
253  }
254 }
255 
257 exit(‪$splitFunctionalTests->execute());
‪FunctionalTestCaseVisitor\getTests
‪getTests()
Definition: splitFunctionalTests.php:242
‪SplitFunctionalTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitFunctionalTests.php:175
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitFunctionalTests.php:256
‪SplitFunctionalTests\execute
‪execute()
Definition: splitFunctionalTests.php:62
‪$parser
‪$parser
Definition: annotationChecker.php:103
‪FunctionalTestCaseVisitor\getFqcn
‪getFqcn()
Definition: splitFunctionalTests.php:250
‪FunctionalTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitFunctionalTests.php:204
‪SplitFunctionalTests
Definition: splitFunctionalTests.php:58
‪FunctionalTestCaseVisitor\$tests
‪array $tests
Definition: splitFunctionalTests.php:193
‪$_SERVER
‪$_SERVER['TYPO3_DEPRECATED_ENTRYPOINT']
Definition: legacy-backend.php:20
‪FunctionalTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitFunctionalTests.php:198
‪$output
‪$output
Definition: annotationChecker.php:114
‪FunctionalTestCaseVisitor
Definition: splitFunctionalTests.php:189