Skip to content

Implement TestCase->expectProcessExit($exitCode) #6275

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
33 changes: 33 additions & 0 deletions ProcessExitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;

final class ProcessExitTest extends TestCase
{
#[RunInSeparateProcess]
#[DataProvider("provideExitCodes")]
public function testOne(?int $expectedExit, int $actualExitCode): void
{
if ($expectedExit !== null) {
$this->expectProcessExit($expectedExit);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should add a PHPUnit error/warning when expectProcessExit is called in a test, which does not use process-isolation

}

exit($actualExitCode);
}

static public function provideExitCodes():iterable {
yield [null, 0];
yield [null, 1];

yield [0, 0];
yield [0, 1];

yield [1, 1];
yield [1, 0];
}
}
11 changes: 11 additions & 0 deletions src/Framework/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ abstract class TestCase extends Assert implements Reorderable, SelfDescribing, T
private bool $outputRetrievedForAssertion = false;
private bool $doesNotPerformAssertions = false;
private bool $expectErrorLog = false;
private ?int $expectProcessExit = null;

/**
* @var list<Comparator>
Expand Down Expand Up @@ -960,6 +961,11 @@ final public function wasPrepared(): bool
return $this->wasPrepared;
}

final public function getExpectedProcessExitCode(): ?int
{
return $this->expectProcessExit;
}

/**
* Returns a matcher that matches when the method is executed
* zero or more times.
Expand Down Expand Up @@ -1044,6 +1050,11 @@ final protected function expectOutputString(string $expectedString): void
$this->outputExpectedString = $expectedString;
}

final protected function expectProcessExit(int $exitCode): void
{
$this->expectProcessExit = $exitCode;
}

final protected function expectErrorLog(): void
{
$this->expectErrorLog = true;
Expand Down
26 changes: 19 additions & 7 deletions src/Framework/TestRunner/ChildProcessResultProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ public function __construct(Facade $eventFacade, Emitter $emitter, PassedTests $
$this->codeCoverage = $codeCoverage;
}

public function process(Test $test, string $serializedProcessResult, string $stderr): void
public function process(Test $test, string $serializedProcessResult, string $stderr, int $exitCode): void
{
assert($test instanceof TestCase);

if ($stderr !== '') {
$exception = new Exception(trim($stderr));

assert($test instanceof TestCase);

$this->emitter->testErrored(
TestMethodBuilder::fromTestCase($test),
ThrowableBuilder::from($exception),
Expand All @@ -59,8 +59,6 @@ public function process(Test $test, string $serializedProcessResult, string $std

$exception = new AssertionFailedError('Test was run in child process and ended unexpectedly');

assert($test instanceof TestCase);

$this->emitter->testErrored(
TestMethodBuilder::fromTestCase($test),
ThrowableBuilder::from($exception),
Expand All @@ -74,11 +72,25 @@ public function process(Test $test, string $serializedProcessResult, string $std
return;
}

try {
if ($childResult->expectedProcessExit !== null && $childResult->testCalledExit === true) {
Assert::assertSame($childResult->expectedProcessExit, $exitCode, 'Process exit-code expectation failed');
} elseif ($childResult->expectedProcessExit !== null && $childResult->testCalledExit === false) {
Assert::fail('Process expected exit() to be called but test did not call it');
} elseif ($childResult->expectedProcessExit === null && $childResult->testCalledExit === true) {
Assert::fail('Process called exit() but the test did not expect it');
}
} catch (AssertionFailedError $e) {
$this->emitter->testFailed(
TestMethodBuilder::fromTestCase($test),
ThrowableBuilder::from($e),
null,
);
}

$this->eventFacade->forward($childResult->events);
$this->passedTests->import($childResult->passedTests);

assert($test instanceof TestCase);

$test->setResult($childResult->testResult);
$test->addToAssertionCount($childResult->numAssertions);

Expand Down
37 changes: 21 additions & 16 deletions src/Framework/TestRunner/templates/class.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,30 @@ function __phpunit_run_isolated_test()
$test->setInIsolation(true);

ob_end_clean();
$output = '';

$test->run();
$testCalledExit = true;
register_shutdown_function(function() use ($test, $output, $dispatcher, &$testCalledExit) {
file_put_contents(
'{processResultFile}',
serialize(
(object)[
'testResult' => $test->result(),
'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null,
'numAssertions' => $test->numberOfAssertionsPerformed(),
'testCalledExit' => $testCalledExit,
'expectedProcessExit' => $test->getExpectedProcessExitCode(),
'output' => $output,
'events' => $dispatcher->flush(),
'passedTests' => PassedTests::instance(),
]
)
);
});

$output = '';
$test->run();

$testCalledExit = false;
if (!$test->expectsOutput()) {
$output = $test->output();
}
Expand All @@ -98,20 +117,6 @@ function __phpunit_run_isolated_test()
@rewind(STDOUT);
}
}

file_put_contents(
'{processResultFile}',
serialize(
(object)[
'testResult' => $test->result(),
'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null,
'numAssertions' => $test->numberOfAssertionsPerformed(),
'output' => $output,
'events' => $dispatcher->flush(),
'passedTests' => PassedTests::instance()
]
)
);
}

function __phpunit_error_handler($errno, $errstr, $errfile, $errline)
Expand Down
37 changes: 21 additions & 16 deletions src/Framework/TestRunner/templates/method.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,30 @@ function __phpunit_run_isolated_test()
$test->setInIsolation(true);

ob_end_clean();
$output = '';

$test->run();
$testCalledExit = true;
register_shutdown_function(function() use ($test, $output, $dispatcher, &$testCalledExit) {
file_put_contents(
'{processResultFile}',
serialize(
(object)[
'testResult' => $test->result(),
'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null,
'numAssertions' => $test->numberOfAssertionsPerformed(),
'testCalledExit' => $testCalledExit,
'expectedProcessExit' => $test->getExpectedProcessExitCode(),
'output' => $output,
'events' => $dispatcher->flush(),
'passedTests' => PassedTests::instance(),
]
)
);
});

$output = '';
$test->run();

$testCalledExit = false;
if (!$test->expectsOutput()) {
$output = $test->output();
}
Expand All @@ -98,20 +117,6 @@ function __phpunit_run_isolated_test()
@rewind(STDOUT);
}
}

file_put_contents(
'{processResultFile}',
serialize(
(object)[
'testResult' => $test->result(),
'codeCoverage' => {collectCodeCoverageInformation} ? CodeCoverage::instance()->codeCoverage() : null,
'numAssertions' => $test->numberOfAssertionsPerformed(),
'output' => $output,
'events' => $dispatcher->flush(),
'passedTests' => PassedTests::instance()
]
)
);
}

function __phpunit_error_handler($errno, $errstr, $errfile, $errline)
Expand Down
10 changes: 9 additions & 1 deletion src/Util/PHP/DefaultJobRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use function is_array;
use function is_resource;
use function proc_close;
use function proc_get_status;
use function proc_open;
use function stream_get_contents;
use function sys_get_temp_dir;
Expand Down Expand Up @@ -147,6 +148,13 @@ private function runProcess(Job $job, ?string $temporaryFile): Result
fclose($pipes[2]);
}

$exitCode = 0;
$processStatus = proc_get_status($process);

if ($processStatus['running'] === false) {
$exitCode = $processStatus['exitcode'];
}

proc_close($process);

if ($temporaryFile !== null) {
Expand All @@ -156,7 +164,7 @@ private function runProcess(Job $job, ?string $temporaryFile): Result
assert($stdout !== false);
assert($stderr !== false);

return new Result($stdout, $stderr);
return new Result($stdout, $stderr, $exitCode);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Util/PHP/JobRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ final public function runTestJob(Job $job, string $processResultFile, Test $test
$test,
$processResult,
$result->stderr(),
$result->exitCode(),
);

EventFacade::emitter()->childProcessFinished($result->stdout(), $result->stderr());
Expand Down
13 changes: 10 additions & 3 deletions src/Util/PHP/Result.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@
{
private string $stdout;
private string $stderr;
private int $exitCode;

public function __construct(string $stdout, string $stderr)
public function __construct(string $stdout, string $stderr, int $exitCode)
{
$this->stdout = $stdout;
$this->stderr = $stderr;
$this->stdout = $stdout;
$this->stderr = $stderr;
$this->exitCode = $exitCode;
}

public function stdout(): string
Expand All @@ -36,4 +38,9 @@ public function stderr(): string
{
return $this->stderr;
}

public function exitCode(): int
{
return $this->exitCode;
}
}
34 changes: 34 additions & 0 deletions tests/_files/SeparateProcessesExpectedExitTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TestFixture;

use PHPUnit\Framework\Attributes\RunInSeparateProcess;
use PHPUnit\Framework\TestCase;

final class SeparateProcessesExpectedExitTest extends TestCase
{
#[RunInSeparateProcess]
public function testExitExpectationMatched(): void
{
$this->expectProcessExit(0);
$this->assertTrue(true);

exit(0);
}

#[RunInSeparateProcess]
public function testWrongExitExpectation(): void
{
$this->expectProcessExit(0);
$this->assertTrue(true);

exit(1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
--TEST--
phpunit --no-configuration ../../_files/SeparateProcessesTest.php
--FILE--
<?php declare(strict_types=1);
$_SERVER['argv'][] = '--do-not-cache-result';
$_SERVER['argv'][] = '--no-configuration';
$_SERVER['argv'][] = __DIR__ . '/../../_files/SeparateProcessesExpectedExitTest.php';

require_once __DIR__ . '/../../bootstrap.php';
(new PHPUnit\TextUI\Application)->run($_SERVER['argv']);
--EXPECTF--
PHPUnit %s by Sebastian Bergmann and contributors.

Runtime: %s

EE 2 / 2 (100%)

Time: %s, Memory: %s

There were 2 errors:

1) PHPUnit\TestFixture\SeparateProcessesTest::testFoo
Process called exit() but the test did not expect it

2) PHPUnit\TestFixture\SeparateProcessesTest::testBar
Process called exit() but the test did not expect it

ERRORS!
Tests: 2, Assertions: 0, Errors: 2.
6 changes: 3 additions & 3 deletions tests/end-to-end/generic/separate-processes-test.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ PHPUnit %s by Sebastian Bergmann and contributors.

Runtime: %s

EE 2 / 2 (100%)
EE 2 / 2 (100%)

Time: %s, Memory: %s

There were 2 errors:

1) PHPUnit\TestFixture\SeparateProcessesTest::testFoo
Test was run in child process and ended unexpectedly
Process called exit() but the test did not expect it

2) PHPUnit\TestFixture\SeparateProcessesTest::testBar
Test was run in child process and ended unexpectedly
Process called exit() but the test did not expect it

ERRORS!
Tests: 2, Assertions: 0, Errors: 2.
Loading
Loading