[BUGFIX] Apply enableFields in JOIN's ON condition
[Packages/TYPO3.CMS.git] / Build / Scripts / splitAcceptanceTests.php
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
39 /**
40 * This script is typically executed by runTests.sh.
41 *
42 * The script expects to be run from the core root:
43 * ./Build/Scripts/splitAcceptanceTests.php <numberOfChunks>
44 *
45 * Verbose output with 8 chunks:
46 * ./Build/Scripts/splitAcceptanceTests.php 8 -v
47 *
48 * It's purpose is to find all core Backend acceptance tests and split them into
49 * pieces. In CI, there are for example 8 jobs for the ac tests and each picks one
50 * chunk of tests. This way, acceptance tests are run in parallel
51 * and thus reduce the overall runtime of the test suite.
52 *
53 * codeception group files including their specific set of tests are written to:
54 * typo3/sysext/core/Tests/Acceptance/AcceptanceTests-Job-<counter>
55 */
56 class SplitAcceptanceTests extends NodeVisitorAbstract
57 {
58 /**
59 * Main entry method
60 */
61 public function execute()
62 {
63 $input = new ArgvInput($_SERVER['argv'], $this->getInputDefinition());
64 $output = new ConsoleOutput();
65
66 // Number of chunks and verbose output
67 $numberOfChunks = (int)$input->getArgument('numberOfChunks');
68
69 if ($numberOfChunks < 1 || $numberOfChunks > 99) {
70 throw new \InvalidArgumentException(
71 'Main argument "numberOfChunks" must be at least 1 and maximum 99',
72 1528319388
73 );
74 }
75
76 if ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose', true)) {
77 $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
78 }
79
80 // Find functional test files
81 $testFiles = (new Finder())
82 ->files()
83 ->in(__DIR__ . '/../../typo3/sysext/core/Tests/Acceptance/Backend')
84 ->name('/Cest\.php$/')
85 ->sortByName()
86 ;
87
88 $parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
89 $testStats = [];
90 foreach ($testFiles as $file) {
91 /** @var $file SplFileInfo */
92 $relativeFilename = $file->getRealPath();
93 preg_match('/.*typo3\/sysext\/(.*)$/', $relativeFilename, $matches);
94 $relativeFilename = $matches[1];
95
96 $ast = $parser->parse($file->getContents());
97 $traverser = new NodeTraverser();
98 $visitor = new NameResolver();
99 $traverser->addVisitor($visitor);
100 $visitor = new AcceptanceTestCaseVisitor();
101 $traverser->addVisitor($visitor);
102 $traverser->traverse($ast);
103
104 $fqcn = $visitor->getFqcn();
105 $tests = $visitor->getTests();
106 if (!empty($tests)) {
107 $testStats[$relativeFilename] = 0;
108 }
109
110 foreach ($tests as $test) {
111 if (isset($test['dataProvider'])) {
112 // Test uses a data provider - get number of data sets. Data provider methods in codeception
113 // are protected, so we reflect them and make them accessible to see how many test cases they contain.
114 $dataProviderMethodName = $test['dataProvider'];
115 $dataProviderMethod = new \ReflectionMethod($fqcn, $dataProviderMethodName);
116 $dataProviderMethod->setAccessible(true);
117 $numberOfDataSets = count($dataProviderMethod->invoke(new $fqcn()));
118 $testStats[$relativeFilename] += $numberOfDataSets;
119 } else {
120 // Just a single test
121 $testStats[$relativeFilename] += 1;
122 }
123 }
124 }
125
126 // Sort test files by number of tests, descending
127 arsort($testStats);
128
129 $numberOfTestsPerChunk = [];
130 for ($i = 1; $i <= $numberOfChunks; $i++) {
131 $numberOfTestsPerChunk[$i] = 0;
132 }
133
134 foreach ($testStats as $testFile => $numberOfTestsInFile) {
135 // Sort list of tests per chunk by number of tests, pick lowest as
136 // the target of this test file
137 asort($numberOfTestsPerChunk);
138 reset($numberOfTestsPerChunk);
139 $jobFileNumber = key($numberOfTestsPerChunk);
140
141 $content = str_replace('core/Tests/', '', $testFile) . "\n";
142 file_put_contents(__DIR__ . '/../../typo3/sysext/core/Tests/Acceptance/' . 'AcceptanceTests-Job-' . $jobFileNumber, $content, FILE_APPEND);
143
144 $numberOfTestsPerChunk[$jobFileNumber] = $numberOfTestsPerChunk[$jobFileNumber] + $numberOfTestsInFile;
145 }
146
147 if ($output->isVerbose()) {
148 $output->writeln('Number of test files found: ' . count($testStats));
149 $output->writeln('Number of tests found: ' . array_sum($testStats));
150 $output->writeln('Number of chunks prepared: ' . $numberOfChunks);
151 ksort($numberOfTestsPerChunk);
152 foreach ($numberOfTestsPerChunk as $chunkNumber => $testNumber) {
153 $output->writeln('Number of tests in chunk ' . $chunkNumber . ': ' . $testNumber);
154 }
155 }
156 }
157
158 /**
159 * Allowed script arguments
160 *
161 * @return InputDefinition argv input definition of symfony console
162 */
163 private function getInputDefinition(): InputDefinition
164 {
165 return new InputDefinition([
166 new InputArgument('numberOfChunks', InputArgument::REQUIRED, 'Number of chunks / jobs to create'),
167 new InputOption('--verbose', '-v', InputOption::VALUE_NONE, 'Enable verbose output'),
168 ]);
169 }
170 }
171
172 /**
173 * nikic/php-parser node visitor to find test class namespace,
174 * count @test annotated methods and their possible @dataProvider's
175 */
176 class AcceptanceTestCaseVisitor extends NodeVisitorAbstract
177 {
178 /**
179 * @var array[] An array of arrays with test method names and optionally a data provider name
180 */
181 private $tests = [];
182
183 /**
184 * @var string Fully qualified test class name
185 */
186 private $fqcn;
187
188 /**
189 * Create a list of '@test' annotated methods in a test case
190 * file and see if single tests use data providers.
191 *
192 * @param Node $node
193 */
194 public function enterNode(Node $node): void
195 {
196 if ($node instanceof Node\Stmt\Class_
197 && !$node->isAnonymous()
198 ) {
199 // The test class full namespace
200 $this->fqcn = (string)$node->namespacedName;
201 }
202
203 // A method is considered a test method, if:
204 if (// It is a method
205 $node instanceof \PhpParser\Node\Stmt\ClassMethod
206 // There is a method comment
207 && ($docComment = $node->getDocComment()) instanceof Doc
208 // The method is public
209 && $node->isPublic()
210 // The methods does not start with an "_" (eg. _before())
211 && $node->name->name[0] !== '_'
212 ) {
213 // Found a test
214 $test = [
215 'methodName' => $node->name->name,
216 ];
217 preg_match_all(
218 '/\s*\s@(?<annotations>[^\s.].*)\n/',
219 $docComment->getText(),
220 $matches
221 );
222 foreach ($matches['annotations'] as $possibleDataProvider) {
223 // See if this test has a data provider attached
224 if (strpos($possibleDataProvider, 'dataProvider') === 0) {
225 $test['dataProvider'] = trim(ltrim($possibleDataProvider, 'dataProvider'));
226 }
227 }
228 $this->tests[] = $test;
229 }
230 }
231
232 /**
233 * Return array of found tests and their data providers
234 *
235 * @return array
236 */
237 public function getTests(): array
238 {
239 return $this->tests;
240 }
241
242 /**
243 * Return Fully qualified class test name
244 *
245 * @return string
246 */
247 public function getFqcn(): string
248 {
249 return $this->fqcn;
250 }
251 }
252
253 $splitFunctionalTests = new SplitAcceptanceTests();
254 exit($splitFunctionalTests->execute());