Repository navigation
Testbench never runs ConfigureTerminalDimensions, so package tests spawn stty on every console call #686
Description
Before submitting
- I have read the contribution guide in full and this report meets its requirements.
- I have searched existing issues and discussions for this problem.
- I have run the reproduction below and verified the reported failure.
Hypervel package and version
hypervel/testbench and hypervel/foundation, from hypervel/components 0.4 at 2de4209197afbd4eb9afedd9e976a47360a592cf
PHP version
8.4.27 (NTS)
Swoole version
6.3.0RC1
Relevant environment
Docker image based on php:8.4.27-cli-alpine3.24: Alpine 3.24 (musl), Linux 6.18, x86_64, 4 vCPUs. PHPUnit 13.4.3, ParaTest 7.27.4, symfony/console 8.1.9. No database or other external service is involved in the reproduction.
Description
When stdout is not a TTY, ConfigureTerminalDimensions sets COLUMNS=80 and LINES=24 if they are unset, so that Symfony's Terminal does not spawn stty inside Swoole's hooked subprocess wait. Both kernels run it (Console/Kernel.php#L95-L104, Http/Kernel.php#L46-L55). Application tests get it too, because Foundation\Testing\TestCase::createApplication() bootstraps the console kernel (TestCase.php#L87-L108).
Testbench never runs it. CreatesApplication::createApplication() bootstraps the application step by step (#L241-L291, #L673-L720), and the console kernel it binds has no bootstrappers (Testbench Console/Kernel.php#L31-L39). Neither sequence includes ConfigureTerminalDimensions.
So in a package's tests, wherever stdout is not a TTY (ParaTest workers, CI), COLUMNS and LINES are unset. Every Hypervel console command a test runs builds an OutputStyle, a SymfonyStyle, whose Terminal lookup spawns stty -a through the hooked proc_open() inside the test's coroutine. Symfony does not cache a failed lookup, so this happens on every call. In a suite using RefreshDatabase with in-memory SQLite, one file of 18 tests spawned 378, about 21 per test, since migrate:fresh runs before each one.
The Testbench CLI process itself is not affected, because Symfony's Application::run() exports both variables. But the ParaTest workers it starts never see them: Symfony Process passes on only the variables that were present at startup.
This matters because that hooked subprocess wait can hang a worker for good, which is why phpunit.xml.dist pins both variables (#L31-L40). In a package suite we maintain, under CPU load, testbench package:test --parallel hung in 5 of 36 runs. The hung workers we inspected were each waiting in proc_close() for an stty that had already exited. With COLUMNS and LINES pinned in that package's phpunit.xml, it hung in 0 of 31. The hang itself is a Swoole bug, now reported as swoole/swoole-src#6328. Until Testbench runs the bootstrapper, every package has to copy the pin.
Minimal reproduction
This test fails on 2de4209:
<?php
declare(strict_types=1);
use Hypervel\Testbench\TestCase;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
class TerminalDimensionsTest extends TestCase
{
#[RunInSeparateProcess]
public function testTestbenchApplicationsConfigureHeadlessTerminalDimensions(): void
{
putenv('COLUMNS');
putenv('LINES');
$this->assertFalse(stream_isatty(STDOUT));
$this->refreshApplication();
$this->assertSame('80', getenv('COLUMNS'));
$this->assertSame('24', getenv('LINES'));
}
}It passes once Testbench runs the bootstrapper where the kernels do, after the environment is loaded and before configuration. With this change, five Artisan::call('env') calls from a Testbench test also spawn no stty:
--- a/src/testbench/src/Concerns/CreatesApplication.php
+++ b/src/testbench/src/Concerns/CreatesApplication.php
@@ -13,6 +13,7 @@
use Hypervel\Di\Bootstrap\GenerateProxies;
use Hypervel\Foundation\Application;
use Hypervel\Foundation\Bootstrap\BootProviders;
+use Hypervel\Foundation\Bootstrap\ConfigureTerminalDimensions;
use Hypervel\Foundation\Bootstrap\HandleExceptions as FoundationHandleExceptions;
use Hypervel\Foundation\Bootstrap\LoadConfiguration as FoundationLoadConfiguration;
use Hypervel\Foundation\Bootstrap\LoadEnvironmentVariables;
@@ -267,6 +268,7 @@
$this->resolveApplicationBindings($app);
$this->resolveApplicationExceptionHandler($app);
$this->resolveApplicationEnvironmentVariables($app);
+ $app->make(ConfigureTerminalDimensions::class)->bootstrap($app);
$this->resolveApplicationConfiguration($app);
// Must run after resolveApplicationConfiguration() because LoadConfiguration'sSteps to reproduce
-
In any package that requires
hypervel/components0.4.x-dev, save the test above asTerminalDimensionsTest.php. -
Run it without the package's PHPUnit configuration, so no
COLUMNSpin applies:vendor/bin/phpunit --no-configuration --bootstrap vendor/autoload.php --no-coverage TerminalDimensionsTest.php
-
To see the subprocesses as well: put a logging
sttyshim first onPATH, then callArtisan::call('env')five times from a Testbench test. The log gains fivestty -alines.
Expected behavior
A Testbench application created without a TTY has COLUMNS=80 and LINES=24 set, as the console and HTTP kernels and the Foundation test case already ensure. Console calls in package tests spawn no stty.
Actual behavior
1) TerminalDimensionsTest::testTestbenchApplicationsConfigureHeadlessTerminalDimensions
Failed asserting that false is identical to '80'.
Five Artisan::call('env') calls from a Testbench test spawned five stty -a processes, all children of the PHPUnit process.
Activity
fix(testing): clear framework terminal defaults after each test
fix(console): keep headless terminal defaults out of the environment
binaryfire commentedon Oct 24, 2026
Thanks for the detailed report. This has been fixed in #687