Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
52.63% |
30 / 57 |
|
61.11% |
11 / 18 |
CRAP | |
0.00% |
0 / 1 |
PhpUnitXmlManager | |
52.63% |
30 / 57 |
|
61.11% |
11 / 18 |
118.38 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getPhpUnitXmlTarget | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPhpUnitXmlDist | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTestsList | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadPhpUnitXmlDist | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadPhpUnitXml | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadTestClasses | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
scanForTestFiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
extractNamespaceFromFile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
resolveFileForTest | |
28.57% |
4 / 14 |
|
0.00% |
0 / 1 |
19.12 | |||
buildSuites | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isPhpUnitXmlPrepared | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
createPhpUnitXml | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
listTestsNotice | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
splitTestsList | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
splitTestsListExtensions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
splitTestsListDefault | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
splitTestsCustom | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | declare( strict_types = 1 ); |
4 | |
5 | namespace MediaWiki\Composer\PhpUnitSplitter; |
6 | |
7 | /** |
8 | * @license GPL-2.0-or-later |
9 | */ |
10 | class PhpUnitXmlManager { |
11 | |
12 | private string $rootDir; |
13 | |
14 | private string $testsListFile; |
15 | |
16 | /** |
17 | * The `SkippedTestCase` is generated dynamically by PHPUnit for tests |
18 | * that are marked as skipped. We don't need to find a matching filesystem |
19 | * file for these. |
20 | * |
21 | * The `ParserIntegrationTest` is a special case - it's a single test class |
22 | * that generates very many tests. To balance out the test suites, we exclude |
23 | * the class from the scan, and add it back in PhpUnitXml::addSpecialCaseTests |
24 | */ |
25 | private const EXPECTED_MISSING_CLASSES = [ |
26 | "PHPUnit\\Framework\\SkippedTestCase", |
27 | "MediaWiki\\Extension\\Scribunto\\Tests\\Engines\\LuaCommon\\LuaEngineTestSkip", |
28 | "\\ParserIntegrationTest", |
29 | ]; |
30 | |
31 | public function __construct( string $rootDir, string $testsListFile ) { |
32 | $this->rootDir = $rootDir; |
33 | $this->testsListFile = $testsListFile; |
34 | } |
35 | |
36 | private function getPhpUnitXmlTarget(): string { |
37 | return $this->rootDir . DIRECTORY_SEPARATOR . PhpUnitXml::PHP_UNIT_XML_FILE; |
38 | } |
39 | |
40 | private function getPhpUnitXmlDist(): string { |
41 | return $this->rootDir . DIRECTORY_SEPARATOR . "phpunit.xml.dist"; |
42 | } |
43 | |
44 | private function getTestsList(): string { |
45 | return $this->rootDir . DIRECTORY_SEPARATOR . $this->testsListFile; |
46 | } |
47 | |
48 | private function loadPhpUnitXmlDist(): PhpUnitXml { |
49 | return $this->loadPhpUnitXml( $this->getPhpUnitXmlDist() ); |
50 | } |
51 | |
52 | private function loadPhpUnitXml( string $targetFile ): PhpUnitXml { |
53 | return new PhpUnitXml( $targetFile ); |
54 | } |
55 | |
56 | private function loadTestClasses(): array { |
57 | if ( !file_exists( $this->getTestsList() ) ) { |
58 | throw new TestListMissingException( $this->getTestsList() ); |
59 | } |
60 | return ( new PhpUnitTestListProcessor( $this->getTestsList() ) )->getTestClasses(); |
61 | } |
62 | |
63 | private function scanForTestFiles(): array { |
64 | return ( new PhpUnitTestFileScanner( $this->rootDir ) )->scanForFiles(); |
65 | } |
66 | |
67 | private static function extractNamespaceFromFile( $filename ): array { |
68 | $contents = file_get_contents( $filename ); |
69 | $matches = []; |
70 | if ( preg_match( '/^namespace\s+([^\s;]+)/m', $contents, $matches ) ) { |
71 | return explode( '\\', $matches[1] ); |
72 | } |
73 | return []; |
74 | } |
75 | |
76 | /** |
77 | * @param TestDescriptor $testDescriptor |
78 | * @param array $phpFiles |
79 | * @return ?string |
80 | * @throws MissingNamespaceMatchForTestException |
81 | * @throws UnlocatedTestException |
82 | */ |
83 | private function resolveFileForTest( TestDescriptor $testDescriptor, array $phpFiles ): ?string { |
84 | $filename = $testDescriptor->getClassName() . ".php"; |
85 | if ( !array_key_exists( $filename, $phpFiles ) ) { |
86 | if ( !in_array( $testDescriptor->getFullClassname(), self::EXPECTED_MISSING_CLASSES ) ) { |
87 | throw new UnlocatedTestException( $testDescriptor ); |
88 | } else { |
89 | return null; |
90 | } |
91 | } |
92 | if ( count( $phpFiles[$filename] ) === 1 ) { |
93 | return $phpFiles[$filename][0]; |
94 | } |
95 | $possibleNamespaces = []; |
96 | foreach ( $phpFiles[$filename] as $file ) { |
97 | $namespace = self::extractNamespaceFromFile( $file ); |
98 | if ( $namespace === $testDescriptor->getNamespace() ) { |
99 | return $file; |
100 | } |
101 | $possibleNamespaces[] = $namespace; |
102 | } |
103 | throw new MissingNamespaceMatchForTestException( $testDescriptor ); |
104 | } |
105 | |
106 | private function buildSuites( array $testClasses, int $groups ): array { |
107 | return ( new TestSuiteBuilder() )->buildSuites( $testClasses, $groups ); |
108 | } |
109 | |
110 | public function isPhpUnitXmlPrepared(): bool { |
111 | return PhpUnitXml::isPhpUnitXmlPrepared( $this->rootDir . DIRECTORY_SEPARATOR . "phpunit.xml" ); |
112 | } |
113 | |
114 | /** |
115 | * @return void |
116 | * @throws MissingNamespaceMatchForTestException |
117 | * @throws TestListMissingException |
118 | * @throws UnlocatedTestException |
119 | * @throws SuiteGenerationException |
120 | */ |
121 | public function createPhpUnitXml( int $groups ) { |
122 | $unitFile = $this->loadPhpUnitXmlDist(); |
123 | $testFiles = $this->scanForTestFiles(); |
124 | $testClasses = $this->loadTestClasses(); |
125 | $seenFiles = []; |
126 | foreach ( $testClasses as $testDescriptor ) { |
127 | $file = $this->resolveFileForTest( $testDescriptor, $testFiles ); |
128 | if ( is_string( $file ) && !array_key_exists( $file, $seenFiles ) ) { |
129 | $testDescriptor->setFilename( $file ); |
130 | $seenFiles[$file] = 1; |
131 | } |
132 | } |
133 | $suites = $this->buildSuites( $testClasses, $groups - 1 ); |
134 | $unitFile->addSplitGroups( $suites ); |
135 | $unitFile->addSpecialCaseTests( $groups ); |
136 | $unitFile->saveToDisk( $this->getPhpUnitXmlTarget() ); |
137 | } |
138 | |
139 | public static function listTestsNotice() { |
140 | print( PHP_EOL ); |
141 | print( 'Running `phpunit --list-tests-xml` to get a list of expected tests ... ' . PHP_EOL ); |
142 | print( PHP_EOL ); |
143 | } |
144 | |
145 | /** |
146 | * @throws TestListMissingException |
147 | * @throws UnlocatedTestException |
148 | * @throws MissingNamespaceMatchForTestException |
149 | * @throws SuiteGenerationException |
150 | */ |
151 | public static function splitTestsList( string $testListFile ) { |
152 | /** |
153 | * We split into 8 groups here, because experimentally that generates 100% CPU load |
154 | * on developer machines and results in groups that are similar in size to the |
155 | * Parser tests (which we have to run in a group on their own - see T345481) |
156 | */ |
157 | ( new PhpUnitXmlManager( getcwd(), $testListFile ) )->createPhpUnitXml( 8 ); |
158 | print( PHP_EOL . 'Created modified `phpunit.xml` with test suite groups' . PHP_EOL ); |
159 | } |
160 | |
161 | /** |
162 | * @throws TestListMissingException |
163 | * @throws UnlocatedTestException |
164 | * @throws MissingNamespaceMatchForTestException |
165 | * @throws SuiteGenerationException |
166 | */ |
167 | public static function splitTestsListExtensions() { |
168 | self::splitTestsList( 'tests-list-extensions.xml' ); |
169 | } |
170 | |
171 | /** |
172 | * @throws TestListMissingException |
173 | * @throws UnlocatedTestException |
174 | * @throws MissingNamespaceMatchForTestException |
175 | * @throws SuiteGenerationException |
176 | */ |
177 | public static function splitTestsListDefault() { |
178 | self::splitTestsList( 'tests-list-default.xml' ); |
179 | } |
180 | |
181 | /** |
182 | * @throws TestListMissingException |
183 | * @throws UnlocatedTestException |
184 | * @throws MissingNamespaceMatchForTestException |
185 | * @throws SuiteGenerationException |
186 | */ |
187 | public static function splitTestsCustom() { |
188 | if ( $_SERVER["argc"] < 3 ) { |
189 | print( 'Specify a filename to split' . PHP_EOL ); |
190 | exit( 1 ); |
191 | } |
192 | $filename = $_SERVER["argv"][2]; |
193 | self::splitTestsList( $filename ); |
194 | } |
195 | } |