Skip to content
Merged
11 changes: 4 additions & 7 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,14 @@
</extensions>
<php>
<ini name="error_reporting" value="-1"/>
<ini name="display_errors" value="on"/>
<!-- Keep isolated-child startup errors on stderr so PHPUnit reports their cause. -->
<ini name="display_errors" value="stderr"/>
<ini name="display_startup_errors" value="on"/>
<ini name="memory_limit" value="1024M"/>
<server name="HYPERVEL_PARALLEL_TESTING" value="1"/>
<!--
Pin terminal dimensions so Symfony Console never shells out to `stty`
to detect them. Each test runs console output inside a Swoole coroutine
with SWOOLE_HOOK_ALL, which hooks proc_open/proc_close; under ParaTest's
concurrent worker processes that hooked subprocess wait can deadlock in
the reactor (SIGCHLD/child-reaping race), hanging an arbitrary test.
A headless test runner has no real terminal anyway.
Fix terminal dimensions so console-output assertions do not depend
on the developer's terminal size.
-->
<env name="COLUMNS" value="80" force="true"/>
<env name="LINES" value="24" force="true"/>
Expand Down
2 changes: 2 additions & 0 deletions src/docs/porting-from-laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,8 @@ abstract class TestCase extends BaseTestCase

Add Hypervel's PHPUnit extension to the package's `phpunit.xml` file. Orchestra Testbench resets framework state from its test case, while Hypervel resets it from this extension. See [Test State Cleanup](/docs/{{version}}/testing#test-state-cleanup).

Remove the unused `--custom-argument` flag from package test scripts. Hypervel's `package:test` command uses `-c` as a shortcut for `--configuration`, matching PHPUnit. See [Running Package Tests](/docs/{{version}}/testbench#running-package-tests).

For package testing details, see the [Testbench documentation](/docs/{{version}}/testbench).

<a name="testing-coroutine-isolation"></a>
Expand Down
2 changes: 1 addition & 1 deletion src/docs/testbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,7 @@ The command supports the following options:
| Option | Description |
| --- | --- |
| `--without-tty` | Disable TTY output. |
| `--configuration=` | Read PHPUnit configuration from the given XML file. |
| `-c`, `--configuration=` | Read PHPUnit configuration from the given XML file or directory. |
| `--coverage` | Collect code coverage. |
| `--min=` | Fail when coverage is below the given percentage. |
| `--parallel` | Run tests in parallel through ParaTest. |
Expand Down
28 changes: 21 additions & 7 deletions src/foundation/src/Bootstrap/ConfigureTerminalDimensions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,44 @@
namespace Hypervel\Foundation\Bootstrap;

use Hypervel\Contracts\Foundation\Application;
use ReflectionProperty;
use Symfony\Component\Console\Terminal;

/**
* Prevent Symfony Terminal from repeatedly spawning `stty` when headless.
*
* Its fallback dimensions are not cached, so every lookup otherwise enters
* Swoole's hooked subprocess-wait path.
* Symfony does not cache failed dimension probes, so each lookup otherwise
* enters Swoole's hooked subprocess wait, which can hang even after stty exits.
*
* @see https://github.com/swoole/swoole-src/issues/6328
*/
class ConfigureTerminalDimensions
{
/**
* Configure deterministic dimensions for a headless terminal.
*/
public function bootstrap(Application $app): void
{
static::configure();
}

/**
* Configure headless fallback dimensions without modifying the environment.
*/
public static function configure(): void
{
if (defined('STDOUT') && stream_isatty(STDOUT)) {
return;
}

if (getenv('COLUMNS') === false) {
putenv('COLUMNS=80');
}
// Symfony has no dimension setter. Fill its cache so defaults cannot mask
// later environment loads; getWidth()/getHeight() still prefer explicit values.
foreach (['width' => 80, 'height' => 24] as $name => $default) {
$property = new ReflectionProperty(Terminal::class, $name);

if (getenv('LINES') === false) {
putenv('LINES=24');
if ($property->getValue() === null) {
$property->setValue(null, $default);
}
}
}
}
5 changes: 5 additions & 0 deletions src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Hypervel\Database\DatabaseTransactionsManager;
use Hypervel\Database\Eloquent\Model;
use Hypervel\Engine\Coroutine as EngineCoroutine;
use Hypervel\Foundation\Bootstrap\ConfigureTerminalDimensions;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Attributes\PostCondition;
use PHPUnit\Framework\IncompleteTest;
Expand Down Expand Up @@ -45,6 +46,10 @@ trait RunTestsInCoroutine
*/
protected function invokeTestMethod(string $methodName, array $testArguments): mixed
{
// Initialize each test process before it enters hooked coroutines,
// including tests that start their own run().
ConfigureTerminalDimensions::configure();
Comment thread
greptile-apps[bot] marked this conversation as resolved.

if (Coroutine::getCid() !== -1 || ! $this->runsTestsInCoroutine()) {
return parent::invokeTestMethod($methodName, $testArguments);
}
Expand Down
2 changes: 2 additions & 0 deletions src/testbench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Hypervel applies every `#[WithConfig]` value before service providers register.

Hypervel does not forward the parent Testbench CLI application's full runtime environment to `package:test` subprocesses. In package-test mode, package and workbench environment files are copied into the child runtime application, while shell or CI environment variables, PHPUnit XML values, and Testbench YAML `env` values continue to reach package-test child processes through their normal channels.

The `package:test` command uses `-c` as the shortcut for `--configuration`, matching PHPUnit and the application's `test` command. Orchestra's unused `--custom-argument` flag is omitted.

Testbench writes boolean environment values as `true` and `false` instead of `(true)` and `(false)`, so libraries that read environment variables directly can interpret them. Laravel-style `env()` behavior is unchanged.

Hypervel does not use Orchestra's `TESTBENCH_APP_BASE_PATH` channel. Each process owns its runtime application identity through `BASE_PATH`, remote child processes receive the current worker clone through `TESTBENCH_BASE_PATH`, and `APP_BASE_PATH` remains the explicit user override.
Expand Down
2 changes: 2 additions & 0 deletions src/testbench/src/Concerns/CreatesApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -267,6 +268,7 @@ public function createApplication(): ApplicationContract
$this->resolveApplicationBindings($app);
$this->resolveApplicationExceptionHandler($app);
$this->resolveApplicationEnvironmentVariables($app);
$app->make(ConfigureTerminalDimensions::class)->bootstrap($app);
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
$this->resolveApplicationConfiguration($app);

// Must run after resolveApplicationConfiguration() because LoadConfiguration's
Expand Down
6 changes: 4 additions & 2 deletions src/testbench/src/Foundation/Console/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ class TestCommand extends TestCommandBase
{
/**
* The name and signature of the console command.
*
* Orchestra's unused --custom-argument flag is omitted so -c selects the
* same configuration for profiling, cache settings, and the PHPUnit child.
*/
protected ?string $signature = 'package:test
{--without-tty : Disable output to TTY}
{--configuration= : Read configuration from XML file}
{--c|configuration= : Read configuration from XML file}
{--coverage : Indicates whether coverage information should be collected}
{--min= : Indicates the minimum threshold enforcement for coverage}
{--p|parallel : Indicates if the tests should run in parallel}
Expand All @@ -33,7 +36,6 @@ class TestCommand extends TestCommandBase
{--drop-databases : Indicates if the test databases should be dropped}
{--without-databases : Indicates if database configuration should be performed}
{--without-cache : Do not isolate cache and rate limiter prefixes}
{--c|--custom-argument : Add custom env variables}
';

/**
Expand Down
2 changes: 1 addition & 1 deletion src/testing/src/Console/TestCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class TestCommand extends TestCommandBase
*/
protected ?string $signature = 'test
{--without-tty : Disable output to TTY}
{--configuration= : Read configuration from XML file}
{--c|configuration= : Read configuration from XML file}
{--coverage : Indicates whether coverage information should be collected}
{--min= : Indicates the minimum threshold enforcement for coverage}
{--p|parallel : Indicates if the tests should run in parallel}
Expand Down
49 changes: 39 additions & 10 deletions src/testing/src/Console/TestCommandBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,24 @@ protected function usingPest(): bool
public function phpUnitConfigurationFile(): string
{
$configuration = $this->option('configuration');
$configurationFile = str_replace('./', '', is_string($configuration) && $configuration !== '' ? $configuration : 'phpunit.xml');
$configurationFiles = ['phpunit.xml', 'phpunit.dist.xml', 'phpunit.xml.dist'];
$directory = $this->basePath();
$fallback = './';

$file = (new Collection([
$this->basePath($configurationFile),
$this->basePath("{$configurationFile}.dist"),
]))->filter(static fn (string $path): bool => is_file($path))
->first() ?? './';
if (is_string($configuration) && $configuration !== '') {
$fallback = str_starts_with($configuration, DIRECTORY_SEPARATOR) ? $configuration : $this->basePath($configuration);

if (is_dir($fallback)) {
$directory = $fallback;
} else {
$configurationFiles = [$fallback, "{$fallback}.dist"];
}
}

$file = (new Collection($configurationFiles))
->map(fn (string $path): string => str_starts_with($path, DIRECTORY_SEPARATOR) ? $path : join_paths($directory, $path))
->filter(static fn (string $path): bool => is_file($path))
->first() ?? $fallback;

return $this->option('profile') ? $this->profileConfigurationFile($file) : $file;
}
Expand Down Expand Up @@ -291,9 +302,18 @@ protected function paratestArguments(array $options): array
$inputDefinition = new InputDefinition;
Options::setInputDefinition($inputDefinition);
$input = new ArgvInput(['paratest', ...$arguments], $inputDefinition);
$paraTestOptions = Options::fromConsoleInput($input, $this->basePath());

if (! $paraTestOptions->configuration->hasCoverageCacheDirectory()) {
// Options::fromConsoleInput() replaces PHPUnit's active configuration,
// which can break later isolated tests. Only inspect the cache setting here.
$hasCacheDirectory = $input->getOption('cache-directory') !== null;

if (! $hasCacheDirectory && is_file($file)) {
$document = new DOMDocument;
$document->load($file);
$hasCacheDirectory = $document->documentElement?->hasAttribute('cacheDirectory') ?? false;
}

if (! $hasCacheDirectory) {
$arguments[] = '--cache-directory';
$arguments[] = sys_get_temp_dir() . DIRECTORY_SEPARATOR . '__hypervel_test_cache_directory';
}
Expand All @@ -312,6 +332,14 @@ protected function paratestArguments(array $options): array
*/
protected function filterForwardedArguments(array $arguments, array $exact = [], array $prefixes = [], array $valueOptions = []): array
{
// A forwarded configuration shortcut would override the profiled XML file.
$configurationShortcut = $this->getDefinition()->getOption('configuration')->getShortcut();
$configurationShortcut = $configurationShortcut === null ? null : "-{$configurationShortcut}";

if ($configurationShortcut !== null && in_array('--configuration', $valueOptions, true)) {
$valueOptions[] = $configurationShortcut;
}

$filtered = [];
$skipNextArgument = false;

Expand All @@ -329,7 +357,8 @@ protected function filterForwardedArguments(array $arguments, array $exact = [],
continue 2;
}

if (Str::startsWith($argument, "{$option}=")) {
if (Str::startsWith($argument, "{$option}=")
|| ($option === $configurationShortcut && Str::startsWith($argument, $option))) {
continue 2;
}
}
Expand Down Expand Up @@ -513,7 +542,7 @@ protected function profileConfigurationFile(string $file): string
return $this->temporaryConfigurationFile;
}

if ($file === './') {
if (! is_file($file)) {
return $file;
}

Expand Down
Loading
Loading