Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/http/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,8 @@
$minLevel = YII_ENV_TEST ? 1 : 0;

while ($currentLevel > $minLevel) {
if (@ob_end_clean() === false) {

Check warning on line 88 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "Identical": @@ @@ $currentLevel = ob_get_level(); $minLevel = YII_ENV_TEST ? 1 : 0; while ($currentLevel > $minLevel) { - if (@ob_end_clean() === false) { + if (@ob_end_clean() !== false) { ob_clean(); } $currentLevel = ob_get_level();

Check warning on line 88 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "Identical": @@ @@ $currentLevel = ob_get_level(); $minLevel = YII_ENV_TEST ? 1 : 0; while ($currentLevel > $minLevel) { - if (@ob_end_clean() === false) { + if (@ob_end_clean() !== false) { ob_clean(); } $currentLevel = ob_get_level();
// @codeCoverageIgnoreStart
ob_clean();

Check warning on line 89 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ $minLevel = YII_ENV_TEST ? 1 : 0; while ($currentLevel > $minLevel) { if (@ob_end_clean() === false) { - ob_clean(); + } $currentLevel = ob_get_level(); }

Check warning on line 89 in src/http/ErrorHandler.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.3-ubuntu-latest

Escaped Mutant for Mutator "FunctionCallRemoval": @@ @@ $minLevel = YII_ENV_TEST ? 1 : 0; while ($currentLevel > $minLevel) { if (@ob_end_clean() === false) { - ob_clean(); + } $currentLevel = ob_get_level(); }
// @codeCoverageIgnoreEnd
}

$currentLevel = ob_get_level();
Expand Down
4 changes: 2 additions & 2 deletions tests/http/ErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function testClearOutputCleansAllBuffersInNonTestEnvironment(): void
$initialLevel = ob_get_level();

try {
@runkit_constant_redefine('YII_ENV_TEST', false);
@\runkit_constant_redefine('YII_ENV_TEST', false);

$errorHandler = new ErrorHandler();

Expand Down Expand Up @@ -56,7 +56,7 @@ public function testClearOutputCleansAllBuffersInNonTestEnvironment(): void
ob_start();
}

@runkit_constant_redefine('YII_ENV_TEST', true);
@\runkit_constant_redefine('YII_ENV_TEST', true);
}
}

Expand Down
79 changes: 79 additions & 0 deletions tests/http/stateless/ApplicationErrorHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
use yii2\extensions\psrbridge\http\Response;
use yii2\extensions\psrbridge\tests\provider\StatelessApplicationProvider;
use yii2\extensions\psrbridge\tests\support\FactoryHelper;
use yii2\extensions\psrbridge\tests\support\stub\MockerFunctions;
use yii2\extensions\psrbridge\tests\TestCase;

use function array_filter;
use function ini_get;
use function ini_set;
use function is_array;
use function ob_get_level;
use function ob_start;
Expand Down Expand Up @@ -346,6 +349,82 @@ static function ($errno, $errstr, $errfile, $errline) use (&$warningsCaptured):
}
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
#[RequiresPhpExtension('runkit7')]
public function testRenderExceptionSetsDisplayErrorsInDebugMode(): void
{
@\runkit_constant_redefine('YII_ENV_TEST', false);

MockerFunctions::setObEndCleanShouldFail(true);

$bufferBeforeLevel = ob_get_level();

$_SERVER = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => 'site/trigger-exception',
];

ob_start();
echo 'buffer content that should be cleared';
ob_start();
echo 'nested buffer content';

$originalDisplayErrors = ini_get('display_errors');

$app = $this->statelessApplication(
[
'components' => [
'errorHandler' => [
'discardExistingOutput' => true,
'errorAction' => null,
],
],
],
);

$response = $app->handle(FactoryHelper::createServerRequestCreator()->createFromGlobals());

self::assertSame(
500,
$response->getStatusCode(),
"Expected HTTP '500' for route 'site/trigger-exception'.",
);
self::assertSame(
'text/html; charset=UTF-8',
$response->getHeaderLine('Content-Type'),
"Expected Content-Type 'text/html; charset=UTF-8' for route 'site/trigger-exception'.",
);
self::assertSame(
'1',
ini_get('display_errors'),
"'display_errors' should be set to '1' in debug mode when rendering exception.",
);

$bufferAfterLevel = ob_get_level();

self::assertSame(
0,
$bufferAfterLevel,
"Output buffers should be cleared to level '0' when 'discardExistingOutput' is 'true'.",
);

while (ob_get_level() < $bufferBeforeLevel) {
ob_start();
}

self::assertSame(
$bufferBeforeLevel,
ob_get_level(),
'Output buffers should be restored to initial level.',
);

ini_set('display_errors', $originalDisplayErrors);

@\runkit_constant_redefine('YII_ENV_TEST', true);
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*
Expand Down
66 changes: 1 addition & 65 deletions tests/http/stateless/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,11 @@

namespace yii2\extensions\psrbridge\tests\http\stateless;

use PHPUnit\Framework\Attributes\{Group, RequiresPhpExtension};
use PHPUnit\Framework\Attributes\Group;
use yii\base\InvalidConfigException;
use yii2\extensions\psrbridge\tests\support\FactoryHelper;
use yii2\extensions\psrbridge\tests\TestCase;

use function ini_get;
use function ini_set;
use function ob_get_level;
use function ob_start;

#[Group('http')]
final class ApplicationTest extends TestCase
{
Expand All @@ -24,65 +19,6 @@ protected function tearDown(): void
parent::tearDown();
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
#[RequiresPhpExtension('runkit7')]
public function testRenderExceptionSetsDisplayErrorsInDebugMode(): void
{
@\runkit_constant_redefine('YII_ENV_TEST', false);

$initialBufferLevel = ob_get_level();

$_SERVER = [
'REQUEST_METHOD' => 'GET',
'REQUEST_URI' => 'site/trigger-exception',
];

$originalDisplayErrors = ini_get('display_errors');

try {
$app = $this->statelessApplication(
[
'components' => [
'errorHandler' => ['errorAction' => null],
],
],
);

$response = $app->handle(FactoryHelper::createServerRequestCreator()->createFromGlobals());

self::assertSame(
500,
$response->getStatusCode(),
"Expected HTTP '500' for route 'site/trigger-exception'.",
);
self::assertSame(
'text/html; charset=UTF-8',
$response->getHeaderLine('Content-Type'),
"Expected Content-Type 'text/html; charset=UTF-8' for route 'site/trigger-exception'.",
);
self::assertSame(
'1',
ini_get('display_errors'),
"'display_errors' should be set to '1' when YII_DEBUG mode is enabled and rendering exception view.",
);
self::assertStringContainsString(
'yii\base\Exception: Exception error message.',
$response->getBody()->getContents(),
'Response should contain exception details when YII_DEBUG mode is enabled.',
);
} finally {
ini_set('display_errors', $originalDisplayErrors);

while (ob_get_level() < $initialBufferLevel) {
ob_start();
}

@\runkit_constant_redefine('YII_ENV_TEST', true);
}
}

/**
* @throws InvalidConfigException if the configuration is invalid or incomplete.
*/
Expand Down
5 changes: 5 additions & 0 deletions tests/support/MockerExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public static function load(): void
'name' => 'microtime',
'function' => static fn(bool $as_float = false): float|string => MockerFunctions::microtime($as_float),
],
[
'namespace' => 'yii2\extensions\psrbridge\http',
'name' => 'ob_end_clean',
'function' => static fn(): bool => MockerFunctions::ob_end_clean(),
],
[
'namespace' => 'yii2\extensions\psrbridge\adapter',
'name' => 'stream_get_contents',
Expand Down
35 changes: 35 additions & 0 deletions tests/support/stub/MockerFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* - {@see \headers_sent()} (with file/line tracking).
* - {@see \http_response_code()} (get/set response code).
* - {@see \microtime()} (mockable time for timing tests).
* - {@see \ob_end_clean()} (controllable success/failure).
* - {@see \stream_get_contents()} (controllable stream read/failure).
* - {@see \time()} (mockable time for timing tests).
* - Consistent behavior matching PHP native functions for test reliability.
Expand Down Expand Up @@ -84,6 +85,16 @@ final class MockerFunctions
*/
private static int|null $mockedTime = null;

/**
* Tracks the number of times {@see \ob_end_clean()} was called.
*/
private static int $obEndCleanCallCount = 0;

/**
* Indicates whether {@see \ob_end_clean()} should fail.
*/
private static bool $obEndCleanShouldFail = false;

/**
* Tracks the HTTP response code.
*/
Expand Down Expand Up @@ -190,6 +201,18 @@ public static function microtime(bool $as_float = false): float|string
return \microtime($as_float);
}

public static function ob_end_clean(): bool
{
self::$obEndCleanCallCount++;

// simulate failure only on the first call after enabling
if (self::$obEndCleanShouldFail && self::$obEndCleanCallCount === 1) {
return false;
}

return @\ob_end_clean();
}

public static function reset(): void
{
self::$flushedTimes = 0;
Expand All @@ -198,8 +221,11 @@ public static function reset(): void
self::$headersSentFile = '';
self::$headersSentLine = 0;
self::$mockedTime = null;
self::$obEndCleanCallCount = 0;
self::$obEndCleanShouldFail = false;
self::$responseCode = 200;
self::$streamGetContentsShouldFail = false;

self::clearMockedMicrotime();
}

Expand All @@ -225,6 +251,15 @@ public static function setMockedTime(int $time): void
self::$mockedTime = $time;
}

public static function setObEndCleanShouldFail(bool $shouldFail = true): void
{
self::$obEndCleanShouldFail = $shouldFail;

if ($shouldFail) {
self::$obEndCleanCallCount = 0;
}
}

public static function stream_get_contents(mixed $resource, int $maxlength = -1, int $offset = -1): string|false
{
if (self::$streamGetContentsShouldFail) {
Expand Down
Loading