‪TYPO3CMS  ‪main
splitAcceptanceTests.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 
57 class ‪SplitAcceptanceTests extends NodeVisitorAbstract
58 {
62  public function ‪execute(): int
63  {
64  $input = new ArgvInput(‪$_SERVER['argv'], $this->‪getInputDefinition());
65  ‪$output = new ConsoleOutput();
66 
67  // delete any existing split job files first
68  $targetDirectory = __DIR__ . '/../../typo3/sysext/core/Tests/';
69  $targetFileNamePrefix = 'AcceptanceTests-Job-';
70  $filesInTargetDir = Finder::create()->files()->in($targetDirectory)->name($targetFileNamePrefix . '*');
71  foreach ($filesInTargetDir as $file) {
72  unlink($file->getPathname());
73  }
74 
75  // Number of chunks and verbose output
76  $numberOfChunks = (int)$input->getArgument('numberOfChunks');
77 
78  if ($numberOfChunks < 1 || $numberOfChunks > 99) {
79  throw new \InvalidArgumentException(
80  'Main argument "numberOfChunks" must be at least 1 and maximum 99',
81  1528319388
82  );
83  }
84 
85  if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose', true)) {
86  ‪$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
87  }
88 
89  // Find functional test files
90  $testFiles = (new Finder())
91  ->files()
92  ->in(__DIR__ . '/../../typo3/sysext/core/Tests/Acceptance/Application')
93  ->name('/Cest\.php$/')
94  ->sortByName()
95  ;
96 
97  ‪$parser = (new ParserFactory())->createForVersion(PhpVersion::fromComponents(8, 2));
98  $testStats = [];
99  foreach ($testFiles as $file) {
101  $relativeFilename = $file->getRealPath();
102  preg_match('/.*typo3\/sysext\/(.*)$/', $relativeFilename, $matches);
103  $relativeFilename = $matches[1];
104 
105  $ast = ‪$parser->parse($file->getContents());
106  $traverser = new NodeTraverser();
107  $visitor = new NameResolver();
108  $traverser->addVisitor($visitor);
109  $visitor = new ‪AcceptanceTestCaseVisitor();
110  $traverser->addVisitor($visitor);
111  $traverser->traverse($ast);
112 
113  $fqcn = $visitor->getFqcn();
114  $tests = $visitor->getTests();
115  if (!empty($tests)) {
116  $testStats[$relativeFilename] = 0;
117  }
118 
119  foreach ($tests as $test) {
120  if (isset($test['dataProvider'])) {
121  // Test uses a data provider - get number of data sets. Data provider methods in codeception
122  // are protected, so we reflect them and make them accessible to see how many test cases they contain.
123  $dataProviderMethodName = $test['dataProvider'];
124  $dataProviderMethod = new \ReflectionMethod($fqcn, $dataProviderMethodName);
125  $numberOfDataSets = count($dataProviderMethod->invoke(new $fqcn()));
126  $testStats[$relativeFilename] += $numberOfDataSets;
127  } else {
128  // Just a single test
129  $testStats[$relativeFilename] += 1;
130  }
131  }
132  }
133 
134  // Sort test files by number of tests, descending
135  arsort($testStats);
136 
137  $numberOfTestsPerChunk = [];
138  for ($i = 1; $i <= $numberOfChunks; $i++) {
139  $numberOfTestsPerChunk[$i] = 0;
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 
149  $content = str_replace('core/Tests/', '', $testFile) . "\n";
150 
151  file_put_contents($targetDirectory . $targetFileNamePrefix . $jobFileNumber, $content, FILE_APPEND);
152 
153  $numberOfTestsPerChunk[$jobFileNumber] = $numberOfTestsPerChunk[$jobFileNumber] + $numberOfTestsInFile;
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 ‪AcceptanceTestCaseVisitor extends NodeVisitorAbstract
188 {
192  private array ‪$tests = [];
193 
197  private string ‪$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  // A method is considered a test method, if:
215  if (// It is a method
216  $node instanceof \PhpParser\Node\Stmt\ClassMethod
217  // The method is public
218  && $node->isPublic()
219  // The method does not start with an "_" (eg. _before())
220  && $node->name->name[0] !== '_'
221  ) {
222  // Found a test
223  $test = [
224  'methodName' => $node->name->name,
225  ];
226  foreach ($node->getAttrGroups() as $possibleDataProviderAttributeGroup) {
227  foreach ($possibleDataProviderAttributeGroup->attrs as $possibleDataProviderAttribute) {
228  // See if that method has the codeception DataProvider attribute attached, too.
229  $name = $possibleDataProviderAttribute->name->toCodeString();
230  if ($name === '\\Codeception\\Attribute\\DataProvider') {
231  $dataProviderMethodName = $possibleDataProviderAttribute->args[0]->value->value;
232  $test['dataProvider'] = $dataProviderMethodName;
233  }
234  }
235  }
236  $this->tests[] = $test;
237  }
238  }
239 
243  public function ‪getTests(): array
244  {
245  return ‪$this->tests;
246  }
247 
251  public function ‪getFqcn(): string
252  {
253  return ‪$this->fqcn;
254  }
255 }
256 
258 exit(‪$splitFunctionalTests->execute());
‪AcceptanceTestCaseVisitor\getFqcn
‪getFqcn()
Definition: splitAcceptanceTests.php:251
‪SplitAcceptanceTests
Definition: splitAcceptanceTests.php:58
‪AcceptanceTestCaseVisitor
Definition: splitAcceptanceTests.php:188
‪$parser
‪$parser
Definition: annotationChecker.php:103
‪SplitAcceptanceTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitAcceptanceTests.php:174
‪AcceptanceTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitAcceptanceTests.php:205
‪AcceptanceTestCaseVisitor\getTests
‪getTests()
Definition: splitAcceptanceTests.php:243
‪AcceptanceTestCaseVisitor\$tests
‪array $tests
Definition: splitAcceptanceTests.php:192
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitAcceptanceTests.php:257
‪AcceptanceTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitAcceptanceTests.php:197
‪$_SERVER
‪$_SERVER['TYPO3_DEPRECATED_ENTRYPOINT']
Definition: legacy-backend.php:20
‪$output
‪$output
Definition: annotationChecker.php:114
‪SplitAcceptanceTests\execute
‪execute()
Definition: splitAcceptanceTests.php:62