‪TYPO3CMS  10.4
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()
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/Backend')
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 
172  private function ‪getInputDefinition(): InputDefinition
173  {
174  return new InputDefinition([
175  new InputArgument('numberOfChunks', InputArgument::REQUIRED, 'Number of chunks / jobs to create'),
176  new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Enable verbose output'),
177  ]);
178  }
179 }
180 
185 class ‪AcceptanceTestCaseVisitor extends NodeVisitorAbstract
186 {
190  private ‪$tests = [];
191 
195  private ‪$fqcn;
196 
203  public function ‪enterNode(Node $node): void
204  {
205  if ($node instanceof Node\Stmt\Class_
206  && !$node->isAnonymous()
207  ) {
208  // The test class full namespace
209  $this->fqcn = (string)$node->namespacedName;
210  }
211 
212  // A method is considered a test method, if:
213  if (// It is a method
214  $node instanceof \PhpParser\Node\Stmt\ClassMethod
215  // There is a method comment
216  && ($docComment = $node->getDocComment()) instanceof Doc
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  preg_match_all(
227  '/\s*\s@(?<annotations>[^\s.].*)\n/',
228  $docComment->getText(),
229  $matches
230  );
231  foreach ($matches['annotations'] as $possibleDataProvider) {
232  // See if this test has a data provider attached
233  if (strpos($possibleDataProvider, 'dataProvider') === 0) {
234  $test['dataProvider'] = trim(ltrim($possibleDataProvider, 'dataProvider'));
235  }
236  }
237  $this->tests[] = $test;
238  }
239  }
240 
246  public function ‪getTests(): array
247  {
248  return ‪$this->tests;
249  }
250 
256  public function ‪getFqcn(): string
257  {
258  return ‪$this->fqcn;
259  }
260 }
261 
263 exit(‪$splitFunctionalTests->execute());
‪SplitAcceptanceTests
Definition: splitAcceptanceTests.php:57
‪AcceptanceTestCaseVisitor\$tests
‪array[] $tests
Definition: splitAcceptanceTests.php:189
‪AcceptanceTestCaseVisitor\getTests
‪array getTests()
Definition: splitAcceptanceTests.php:244
‪AcceptanceTestCaseVisitor
Definition: splitAcceptanceTests.php:186
‪$parser
‪$parser
Definition: annotationChecker.php:108
‪AcceptanceTestCaseVisitor\getFqcn
‪string getFqcn()
Definition: splitAcceptanceTests.php:254
‪SplitAcceptanceTests\getInputDefinition
‪InputDefinition getInputDefinition()
Definition: splitAcceptanceTests.php:172
‪AcceptanceTestCaseVisitor\enterNode
‪enterNode(Node $node)
Definition: splitAcceptanceTests.php:201
‪$splitFunctionalTests
‪$splitFunctionalTests
Definition: splitAcceptanceTests.php:260
‪AcceptanceTestCaseVisitor\$fqcn
‪string $fqcn
Definition: splitAcceptanceTests.php:193
‪$output
‪$output
Definition: annotationChecker.php:119
‪SplitAcceptanceTests\execute
‪execute()
Definition: splitAcceptanceTests.php:61