Skip to content

Commit 1a5e5c2

Browse files
committed
Job: utilized PHP 8.5 stream_select() fix for true parallel execution on Windows
PHP 8.5 fixed stream_select() to work with proc_open() pipes on Windows (PeekNamedPipe fix). This allows non-blocking check before reading stdout, enabling true parallel test execution.
1 parent 2c9e673 commit 1a5e5c2

File tree

2 files changed

+39
-4
lines changed

2 files changed

+39
-4
lines changed

src/Runner/Job.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
use Tester\Helpers;
1313
use function count, is_array, is_resource;
14-
use const DIRECTORY_SEPARATOR;
14+
use const DIRECTORY_SEPARATOR, PHP_OS_FAMILY, PHP_VERSION_ID;
1515

1616

1717
/**
@@ -130,7 +130,7 @@ public function run(bool $async = false): void
130130
stream_set_blocking($this->stdout, enable: false); // on Windows does not work with proc_open()
131131
} else {
132132
while ($this->isRunning()) {
133-
usleep(self::RunSleep); // stream_select() doesn't work with proc_open()
133+
usleep(self::RunSleep);
134134
}
135135
}
136136
}
@@ -145,7 +145,23 @@ public function isRunning(): bool
145145
return false;
146146
}
147147

148-
$this->test->stdout .= stream_get_contents($this->stdout);
148+
// PHP 8.5+ Windows: stream_select() works with pipes (PeekNamedPipe fix),
149+
if (PHP_OS_FAMILY === 'Windows' && PHP_VERSION_ID >= 80500) {
150+
$read = [$this->stdout];
151+
$w = $e = [];
152+
while (@stream_select($read, $w, $e, 0, 0) > 0) {
153+
$chunk = fread($this->stdout, 8192);
154+
if ($chunk === false || $chunk === '') {
155+
break;
156+
}
157+
$this->test->stdout .= $chunk;
158+
$read = [$this->stdout];
159+
}
160+
} else {
161+
// Linux/macOS: stream_get_contents() works without blocking
162+
// Windows < 8.5: blocks, but is necessary to prevent deadlock when output exceeds pipe buffer (~64KB)
163+
$this->test->stdout .= stream_get_contents($this->stdout);
164+
}
149165

150166
$status = proc_get_status($this->proc);
151167
if ($status['running']) {
@@ -213,4 +229,23 @@ public function getDuration(): ?float
213229
? $this->duration
214230
: null;
215231
}
232+
233+
234+
/**
235+
* Waits for activity on any of the running jobs.
236+
* @param self[] $jobs
237+
*/
238+
public static function waitForActivity(array $jobs): void
239+
{
240+
if (PHP_OS_FAMILY === 'Windows' && PHP_VERSION_ID < 80500) {
241+
usleep(self::RunSleep);
242+
return;
243+
}
244+
245+
$streams = array_filter(array_map(fn($job) => $job->stdout, $jobs));
246+
if ($streams) {
247+
$w = $e = [];
248+
@stream_select($streams, $w, $e, 0, self::RunSleep);
249+
}
250+
}
216251
}

src/Runner/Runner.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public function run(): bool
115115
}
116116

117117
if ($async) {
118-
usleep(Job::RunSleep); // stream_select() doesn't work with proc_open()
118+
Job::waitForActivity($running);
119119
}
120120

121121
foreach ($running as $key => $job) {

0 commit comments

Comments
 (0)