‪TYPO3CMS  ‪main
splitAcceptanceTests.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 
56 class ‪SplitAcceptanceTests extends NodeVisitorAbstract
57 {
61  public function ‪execute(): int
62  {
63  $input = new ArgvInput($_SERVER['argv'], $this->‪getInputDefinition());
64  ‪$output = new ConsoleOutput();
65 
66  // delete any existing split job files first
67  $targetDirectory = __DIR__ . '/../../typo3/sysext/core/Tests/';
68  $targetFileNamePrefix = 'AcceptanceTests-Job-';
69  $filesInTargetDir = Finder::create()->files()->in($targetDirectory)->name($targetFileNamePrefix . '*');
70  foreach ($filesInTargetDir as $file) {
71  unlink($file->getPathname());
72  }
73 
74  // Number of chunks and verbose output
75  $numberOfChunks = (int)$input->getArgument('numberOfChunks');
76 
77  if ($numberOfChunks < 1 || $numberOfChunks > 99) {
78  throw new \InvalidArgumentException(
79  'Main argument "numberOfChunks" must be at least 1 and maximum 99',
80  1528319388
81  );
82  }
83 
84  if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose', true)) {
85  ‪$output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
86  }
87 
88  // Find functional test files
89  $testFiles = (new Finder())
90  ->files()
91  ->in(__DIR__ . '/../../typo3/sysext/core/Tests/Acceptance/Application')
92  ->name('/Cest\.php$/')
93  ->sortByName()
94  ;
95 
96  ‪$parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
97  $testStats = [];
98  foreach ($testFiles as $file) {
100  $relativeFilename = $file->getRealPath();
101  preg_match('/.*typo3\/sysext\/(.*)$/', $relativeFilename, $matches);
102  $relativeFilename = $matches[1];
103 
104  $ast = ‪$parser->parse($file->getContents());
105  $traverser = new NodeTraverser();
106  $visitor = new NameResolver();
107  $traverser->addVisitor($visitor);
108  $visitor = new ‪AcceptanceTestCaseVisitor();
109  $traverser->addVisitor($visitor);
110  $traverser->traverse($ast);
111 
112  $fqcn = $visitor->getFqcn();
113  $tests = $visitor->getTests();
114  if (!empty($tests)) {
115  $testStats[$relativeFilename] = 0;
116  }
117 
118  foreach ($tests as $test) {
119  if (isset($test['dataProvider'])) {
120  // Test uses a data provider - get number of data sets. Data provider methods in codeception
121  // are protected, so we reflect them and make them accessible to see how many test cases they contain.
122  $dataProviderMethodName = $test['dataProvider'];
123  $dataProviderMethod = new \ReflectionMethod($fqcn, $dataProviderMethodName);
124  $numberOfDataSets = count($dataProviderMethod->invoke(new $fqcn()));
125  $testStats[$relativeFilename] += $numberOfDataSets;
126  } else {
127  // Just a single test
128  $testStats[$relativeFilename] += 1;
129  }
130  }
131  }
132 
133  // Sort test files by number of tests, descending
134  arsort($testStats);
135 
136  $numberOfTestsPerChunk = [];
137  for ($i = 1; $i <= $numberOfChunks; $i++) {
138  $numberOfTestsPerChunk[$i] = 0;
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 
148  $content = str_replace('core/Tests/', '', $testFile) . "\n";
149 
150  file_put_contents($targetDirectory . $targetFileNamePrefix . $jobFileNumber, $content, FILE_APPEND);
151 
152  $numberOfTestsPerChunk[$jobFileNumber] = $numberOfTestsPerChunk[$jobFileNumber] + $numberOfTestsInFile;
153  }
154 
155  if (‪$output->isVerbose()) {
156  ‪$output->writeln('Number of test files found: ' . count($testStats));
157  ‪$output->writeln('Number of tests found: ' . array_sum($testStats));
158  ‪$output->writeln('Number of chunks prepared: ' . $numberOfChunks);
159  ksort($numberOfTestsPerChunk);
160  foreach ($numberOfTestsPerChunk as $chunkNumber => $testNumber) {
161  ‪$output->writeln('Number of tests in chunk ' . $chunkNumber . ': ' . $testNumber);
162  }
163  }
164 
165  return 0;
166  }
167 
173  private function ‪getInputDefinition(): InputDefinition
174  {
175  return new InputDefinition([
176  new InputArgument('numberOfChunks', InputArgument::REQUIRED, 'Number of chunks / jobs to create'),
177  new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Enable verbose output'),
178  ]);
179  }
180 }
181 
186 class ‪AcceptanceTestCaseVisitor extends NodeVisitorAbstract
187 {
191  private ‪$tests = [];
192 
196  private ‪$fqcn;
197 
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  // A method is considered a test method, if:
214  if (// It is a method
215  $node instanceof \PhpParser\Node\Stmt\ClassMethod
216  // The method is public
217  && $node->isPublic()
218  // The methods does not start with an "_" (eg. _before())
219  && $node->name->name[0] !== '_'
220  ) {
221  // Found a test
222  $test = [
223  'methodName' => $node->name->name,
224  ];
225  $docComment = $node->getDocComment();
226  if ($docComment instanceof Doc) {
227  preg_match_all(
228  '/\s*\s@(?<annotations>[^\s.].*)\n/',
229  $docComment->getText(),
230  $matches
231  );
232  foreach ($matches['annotations'] as $possibleDataProvider) {
233  // See if this test has a data provider attached
234  if (str_starts_with($possibleDataProvider, 'dataProvider')) {
235  $test['dataProvider'] = trim(ltrim($possibleDataProvider, 'dataProvider'));
236  }
237  }
238  }
239  $this->tests[] = $test;
240  }
241  }
242 
248  public function ‪getTests(): array
249  {
250  return ‪$this->tests;
251  }
252 
258  public function ‪getFqcn(): string
259  {
260  return ‪$this->fqcn;
261  }
262 }
263 
265 exit(‪$splitFunctionalTests->execute());
‪SplitAcceptanceTests
Definition: splitAcceptanceTests.php:57
‪AcceptanceTestCaseVisitor\$tests
‪array[] $tests
Definition: splitAcceptanceTests.php:190
‪AcceptanceTestCaseVisitor\getTests
‪array getTests()
Definition: splitAcceptanceTests.php:246
‪AcceptanceTestCaseVisitor
Definition: splitAcceptanceTests.php:187
‪$parser
‪$parser
Definition: annotationChecker.php:108
‪AcceptanceTestCaseVisitor\getFqcn
‪string getFqcn()
Definition: splitAcceptanceTests.php:256
‪SplitAcceptanceTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitAcceptanceTests.php:173
‪AcceptanceTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitAcceptanceTests.php:202
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitAcceptanceTests.php:262
‪AcceptanceTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitAcceptanceTests.php:194
‪$output
‪$output
Definition: annotationChecker.php:119
‪SplitAcceptanceTests\execute
‪execute()
Definition: splitAcceptanceTests.php:61