Skip to content

Commit cc88efb

Browse files
committed
Merge branch 'feat/test-speed' into 4.x
2 parents 5bb37a5 + c94cea4 commit cc88efb

6 files changed

Lines changed: 140 additions & 61 deletions

File tree

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
}
4040
},
4141
"scripts": {
42-
"lint": "pint",
43-
"test:lint": "pint --test",
42+
"lint": "pint --parallel",
43+
"test:lint": "pint --parallel --test",
4444
"test:unit": "pest",
4545
"test": [
4646
"@test:lint",

phpunit.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@
3232
<directory suffix=".php">./tests/Fixtures</directory>
3333
</include>
3434
</source>
35+
<php>
36+
<env name="__PEST_PLUGIN_ENV" value="testing"/>
37+
</php>
3538
</phpunit>

src/Analyser.php

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Closure;
88
use Pest\TypeCoverage\Support\Cache;
9+
use PHPStan\Analyser\Error;
910
use Pokio\Environment;
1011

1112
/**
@@ -22,22 +23,107 @@ final class Analyser
2223
public static function analyse(array $files, Closure $postProcessedFile, Closure $onProcessedFile, Cache $cache): void
2324
{
2425
$testCase = new TestCaseForTypeCoverage('dummy');
25-
$chunkOfFiles = Environment::supportsFork() ? array_chunk($files, Environment::maxProcesses()) : [$files];
26-
$promisses = [];
2726

28-
foreach ($chunkOfFiles as $files) {
29-
$promisses[] = async(function () use ($cache, $files, $testCase, $onProcessedFile) {
27+
if (count($files) === 0) {
28+
return;
29+
}
30+
31+
$filesTouched = [];
32+
$filesInCache = [];
33+
34+
foreach ($files as $file) {
35+
if ($cache->has($file)) {
36+
$filesInCache[] = $file;
37+
} else {
38+
$filesTouched[] = $file;
39+
}
40+
}
41+
42+
unset($files);
43+
44+
self::analyseChunks(
45+
[$filesInCache],
46+
$testCase,
47+
$postProcessedFile,
48+
$onProcessedFile,
49+
$cache,
50+
false,
51+
);
52+
53+
// next, if we don't have touched files, we can return early
54+
55+
if (count($filesTouched) === 0) {
56+
return;
57+
}
58+
59+
// if not, lets warm up the cache with the first file:
60+
61+
$firstFile = array_shift($filesTouched);
62+
self::analyseChunks(
63+
[[$firstFile]],
64+
$testCase,
65+
$postProcessedFile,
66+
$onProcessedFile,
67+
$cache,
68+
false,
69+
);
70+
71+
$maxProcesses = Environment::maxProcesses() / 3;
72+
$maxProcesses = max(1, $maxProcesses);
73+
74+
$chunkOfFiles = array_fill(0, $maxProcesses, []);
75+
foreach (array_values($filesTouched) as $i => $file) {
76+
$chunkOfFiles[$i % $maxProcesses][] = $file;
77+
}
78+
79+
$chunkOfFiles = array_values(
80+
array_filter($chunkOfFiles, static fn (array $chunk) => count($chunk) > 0),
81+
);
82+
83+
self::analyseChunks(
84+
$chunkOfFiles,
85+
$testCase,
86+
$postProcessedFile,
87+
$onProcessedFile,
88+
$cache
89+
);
90+
}
91+
92+
/**
93+
* Analyse the chunks of files.
94+
*/
95+
private static function analyseChunks(
96+
array $chunks,
97+
TestCaseForTypeCoverage $testCase,
98+
Closure $postProcessedFile,
99+
Closure $onProcessedFile,
100+
Cache $cache,
101+
bool $useAsync = true,
102+
): void {
103+
$promises = [];
104+
105+
if ($useAsync === false) {
106+
pokio()->useSync();
107+
} else {
108+
if (Environment::supportsFork() && ! isset($_ENV['__PEST_PLUGIN_ENV'])) {
109+
pokio()->useFork();
110+
}
111+
}
112+
113+
foreach ($chunks as $files) {
114+
$promises[] = async(function () use ($files, $testCase, $onProcessedFile) {
115+
$testCase->resetIgnoredErrors();
30116
$results = [];
31117

32-
foreach ($files as $file) {
33-
[$file, $errors, $ignored] = $cache->get($file, function () use ($file, $testCase) {
34-
$testCase->resetIgnoredErrors();
118+
$analyserErrors = $testCase->gatherAnalyserErrors($files);
119+
$analyserIgnored = $testCase->getIgnoredErrors();
35120

36-
$errors = $testCase->gatherAnalyserErrors([$file]);
37-
$ignored = $testCase->getIgnoredErrors();
121+
foreach ($files as $file) {
122+
$errors = array_filter($analyserErrors, static fn (Error $error) => $error->getFile() === $file);
123+
$ignored = array_filter($analyserIgnored, static fn (Error $error) => $error->getFile() === $file);
38124

39-
return [$file, $errors, $ignored];
40-
});
125+
$errors = array_values($errors);
126+
$ignored = array_values($ignored);
41127

42128
$result = Result::fromPHPStanErrors($file, $errors, $ignored);
43129

@@ -50,7 +136,7 @@ public static function analyse(array $files, Closure $postProcessedFile, Closure
50136
});
51137
}
52138

53-
foreach (await($promisses) as $results) {
139+
foreach (await($promises) as $results) {
54140
foreach ($results as $result) {
55141
$postProcessedFile($result);
56142
}

src/Result.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,18 @@ public static function fromPHPStanErrors(string $file, array $phpstanErrors, arr
6868
$constantsCoverage = 100;
6969

7070
foreach ($phpstanErrors as $error) {
71-
if (str_contains($message = $error->getMessage(), 'property types')) {
72-
$propertyCoverage = (int) explode(' ', explode('only ', $message)[1])[2];
71+
if (str_contains($error->getMessage(), 'property types')) {
72+
$propertyCoverage = 0;
7373
}
7474
if (str_contains($error->getMessage(), 'param types')) {
75-
$paramCoverage = (int) explode(' ', explode('only ', $message)[1])[2];
75+
$paramCoverage = 0;
7676
}
7777
if (str_contains($error->getMessage(), 'return types')) {
78-
$returnTypeCoverage = (int) explode(' ', explode('only ', $message)[1])[2];
78+
$returnTypeCoverage = 0;
7979
}
8080

8181
if (str_contains($error->getMessage(), 'constant types')) {
82-
$constantsCoverage = (int) explode(' ', explode('only ', $message)[1])[2];
82+
$constantsCoverage = 0;
8383
}
8484
}
8585

src/Support/Cache.php

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,39 +25,19 @@ public static function instance(): self
2525
}
2626

2727
/**
28-
* Gets the cache contents.
29-
*
30-
* @param callable(): array $callback
31-
* @return array<int, string>
28+
* Checks if the cache contains the given file.
3229
*/
33-
public function get(string $file, callable $callback): array
30+
public function has(string $file): bool
3431
{
3532
$fileHash = md5_file($file);
33+
3634
if ($fileHash === false) {
37-
return $callback();
35+
return false;
3836
}
3937

4038
$items = $this->all();
4139

42-
if (array_key_exists($fileHash, $items)) {
43-
return $items[$fileHash];
44-
}
45-
46-
$values = $callback();
47-
48-
foreach ($values as $value) {
49-
if (is_array($value)) {
50-
foreach ($value as $item) {
51-
if ($item instanceof Error) {
52-
(fn () => $this->canBeIgnored = null)->call($item);
53-
}
54-
}
55-
}
56-
}
57-
58-
$this->persist(md5_file($file), $values);
59-
60-
return $values;
40+
return array_key_exists($fileHash, $items);
6141
}
6242

6343
/**
@@ -102,14 +82,24 @@ private function all(): array
10282
/**
10383
* Persists the cache contents.
10484
*/
105-
private function persist(string $key, array $values): void
85+
public function persist(string $key, array $values): void
10686
{
87+
foreach ($values as $value) {
88+
if (is_array($value)) {
89+
foreach ($value as $item) {
90+
if ($item instanceof Error) {
91+
(fn () => $this->canBeIgnored = null)->call($item);
92+
}
93+
}
94+
}
95+
}
96+
10797
$dirPath = dirname($this->file());
10898
if (! is_dir($dirPath)) {
109-
if (! mkdir($dirPath, 0777, true)) {
99+
if (! mkdir($dirPath, 0755, true)) {
110100
return;
111101
}
112-
chmod($dirPath, 0777);
102+
chmod($dirPath, 0755);
113103
}
114104

115105
$this->withinLock(function () use ($key, $values) {
@@ -145,8 +135,8 @@ private function withinLock(callable $callback): mixed
145135
$dirPath = dirname($filePath);
146136

147137
if (! is_dir($dirPath)) {
148-
mkdir($dirPath, 0777, true);
149-
chmod($dirPath, 0777);
138+
mkdir($dirPath, 0755, true);
139+
chmod($dirPath, 0755);
150140
}
151141

152142
if (! is_file($lockPath)) {

tests/Plugin.php

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public function exit(int $code): never
1919
expect(fn () => $plugin->handleOriginalArguments(['--type-coverage', '--no-cache']))->toThrow(Exception::class, 0)
2020
->and($output->fetch())->toContain(
2121
'.. 100%',
22-
'.. pr12 87',
23-
'.. co14, pr16, pa18, pa18, rt18 12',
24-
'.. co14 87',
22+
'.. pr12 75',
23+
'.. co14, pr16, pa18, pa18, rt18 0',
24+
'.. co14 75',
2525
'.. rt12 75',
26-
'.. pa12 87',
26+
'.. pa12 75',
2727
);
2828
});
2929

@@ -40,11 +40,11 @@ public function exit(int $code): never
4040
expect(fn () => $plugin->handleOriginalArguments(['--type-coverage']))->toThrow(Exception::class, 0)
4141
->and($output->fetch())->toContain(
4242
'.. 100%',
43-
'.. pr12 87',
44-
'.. co14, pr16, pa18, pa18, rt18 12',
45-
'.. co14 87',
43+
'.. pr12 75',
44+
'.. co14, pr16, pa18, pa18, rt18 0',
45+
'.. co14 75',
4646
'.. rt12 75',
47-
'.. pa12 87',
47+
'.. pa12 75',
4848
);
4949
});
5050

@@ -60,11 +60,11 @@ public function exit(int $code): never
6060

6161
expect(fn () => $plugin->handleOriginalArguments(['--type-coverage', '--compact']))->toThrow(Exception::class, 0)
6262
->and($output->fetch())->toContain(
63-
'.. pr12 87',
64-
'.. co14, pr16, pa18, pa18, rt18 12',
65-
'.. co14 87',
63+
'.. pr12 75',
64+
'.. co14, pr16, pa18, pa18, rt18 0',
65+
'.. co14 75',
6666
'.. rt12 75',
67-
'.. pa12 87',
67+
'.. pa12 75',
6868
)->not->toContain('.. 100%');
6969
});
7070

0 commit comments

Comments
 (0)