Skip to content

Testbench never runs ConfigureTerminalDimensions, so package tests spawn stty on every console call #686

Closed
hypervel/components-backup
#71

Description

@dangerdd1

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's

Steps to reproduce

  1. In any package that requires hypervel/components 0.4.x-dev, save the test above as TerminalDimensionsTest.php.

  2. Run it without the package's PHPUnit configuration, so no COLUMNS pin applies:

    vendor/bin/phpunit --no-configuration --bootstrap vendor/autoload.php --no-coverage TerminalDimensionsTest.php
  3. To see the subprocesses as well: put a logging stty shim first on PATH, then call Artisan::call('env') five times from a Testbench test. The log gains five stty -a lines.

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

  1. binaryfire commented on Oct 24, 2026

    @binaryfire
    Member

    Thanks for the detailed report. This has been fixed in #687

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions