‪TYPO3CMS  11.5
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/Acceptance/';
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  $dataProviderMethod->setAccessible(true);
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 ‪$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  // 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 methods 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  $docComment = $node->getDocComment();
227  if ($docComment instanceof Doc) {
228  preg_match_all(
229  '/\s*\s@(?<annotations>[^\s.].*)\n/',
230  $docComment->getText(),
231  $matches
232  );
233  foreach ($matches['annotations'] as $possibleDataProvider) {
234  // See if this test has a data provider attached
235  if (strpos($possibleDataProvider, 'dataProvider') === 0) {
236  $test['dataProvider'] = trim(ltrim($possibleDataProvider, 'dataProvider'));
237  }
238  }
239  }
240  $this->tests[] = $test;
241  }
242  }
243 
249  public function ‪getTests(): array
250  {
251  return ‪$this->tests;
252  }
253 
259  public function ‪getFqcn(): string
260  {
261  return ‪$this->fqcn;
262  }
263 }
264 
266 exit(‪$splitFunctionalTests->execute());
‪SplitAcceptanceTests
Definition: splitAcceptanceTests.php:57
‪AcceptanceTestCaseVisitor\$tests
‪array[] $tests
Definition: splitAcceptanceTests.php:191
‪AcceptanceTestCaseVisitor\getTests
‪array getTests()
Definition: splitAcceptanceTests.php:247
‪AcceptanceTestCaseVisitor
Definition: splitAcceptanceTests.php:188
‪$parser
‪$parser
Definition: annotationChecker.php:110
‪AcceptanceTestCaseVisitor\getFqcn
‪string getFqcn()
Definition: splitAcceptanceTests.php:257
‪SplitAcceptanceTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitAcceptanceTests.php:174
‪AcceptanceTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitAcceptanceTests.php:203
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitAcceptanceTests.php:263
‪AcceptanceTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitAcceptanceTests.php:195
‪$output
‪$output
Definition: annotationChecker.php:121
‪SplitAcceptanceTests\execute
‪execute()
Definition: splitAcceptanceTests.php:61