Skip to content

Commit

Permalink
Handle Ctrl-C cleanly
Browse files Browse the repository at this point in the history
  • Loading branch information
pczerkas committed May 4, 2022
1 parent e280f1f commit d77712f
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 10 deletions.
3 changes: 3 additions & 0 deletions src/Process/CommandLine.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class CommandLine
/** @var PHPUnitBinFile */
protected $phpUnitBin;

/** @var ChunkSize */
protected $chunkSize;

public function __construct(
PHPUnitBinFile $phpUnitBin,
ChunkSize $chunkSize
Expand Down
30 changes: 22 additions & 8 deletions src/Runner/ChunkFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Paraunit\Runner;

use Paraunit\Configuration\PHPUnitConfig;
use Paraunit\Process\AbstractParaunitProcess;
use Paraunit\Proxy\PHPUnitUtilXMLProxy;

class ChunkFile
Expand All @@ -24,16 +25,22 @@ public function __construct(
}

/**
* @return string
* @param array<int, string> $files
*/
public function createChunkFile(
int $chunkNumber,
array $files
): string {
$fileFullPath = $this->phpunitConfig->getFileFullPath();
$document = $this->utilXml->loadFile($this->phpunitConfig->getFileFullPath());
$xpath = new \DOMXPath($document);

$nodeList = $xpath->query('testsuites');

if (! $nodeList) {
throw new \InvalidArgumentException('No testsuites node found in the PHPUnit configuration in ' . $fileFullPath);
}

foreach ($nodeList as $testSuitesNode) {
if (! $testSuitesNode instanceof \DOMElement) {
throw new \InvalidArgumentException('Invalid DOM subtype in PHPUnit configuration, expeding \DOMElement, got ' . get_class($testSuitesNode));
Expand All @@ -53,26 +60,33 @@ public function createChunkFile(
$newTestSuitesNode = $document->createElement('testsuites');
$newTestSuitesNode->appendChild($newTestSuiteNode);

$testSuitesNode->parentNode->replaceChild($newTestSuitesNode, $testSuitesNode);
$parentNode = $testSuitesNode->parentNode;
if ($parentNode instanceof \DOMElement) {
$parentNode->replaceChild($newTestSuitesNode, $testSuitesNode);
}
break;
}

$chunkFileName = $this->getChunkFileName($chunkNumber);
$chunkFileName = $this->getChunkFileName($fileFullPath, $chunkNumber);
$document->save($chunkFileName);

return $chunkFileName;
}

/**
* @return string
*/
private function getChunkFileName(int $chunkNumber): string
public function getChunkFileName(string $fileName, int $chunkNumber): string
{
$fileName = $this->phpunitConfig->getFileFullPath();
$dirname = dirname($fileName);
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
$baseName = basename($fileName, ".{$extension}");

return $dirname . DIRECTORY_SEPARATOR . "{$baseName}_{$chunkNumber}.{$extension}";
}

public function deleteChunkFile(AbstractParaunitProcess $process): void
{
$filename = $process->getFilename();
if (file_exists($filename)) {
unlink($filename);
}
}
}
8 changes: 8 additions & 0 deletions src/Runner/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ public function getNumber(): int
return $this->number;
}

/**
* @return AbstractParaunitProcess|null
*/
public function getProcess()
{
return $this->process;
}

private function handleProcessTermination(): void
{
if ($this->process) {
Expand Down
16 changes: 16 additions & 0 deletions src/Runner/PipelineCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ public function isEmpty(): bool
return true;
}

/**
* @return array<int, AbstractParaunitProcess>
*/
public function getRunningProcesses(): array
{
$processes = [];
foreach ($this->pipelines as $pipeline) {
$process = $pipeline->getProcess();
if ($process) {
$processes[] = $process;
}
}

return $processes;
}

public function triggerProcessTermination(): void
{
foreach ($this->pipelines as $pipeline) {
Expand Down
27 changes: 26 additions & 1 deletion src/Runner/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use Paraunit\Process\ProcessFactoryInterface;
use Paraunit\Runner\ChunkFile;
use Psr\EventDispatcher\EventDispatcherInterface;
use RuntimeException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class Runner implements EventSubscriberInterface
Expand Down Expand Up @@ -59,6 +60,11 @@ public function __construct(
$this->chunkFile = $chunkFile;
$this->queuedProcesses = new \SplQueue();
$this->exitCode = 0;

if (function_exists('pcntl_async_signals') && function_exists('pcntl_signal')) {
pcntl_async_signals(true);
pcntl_signal(SIGINT, [$this, 'onShutdown']);
}
}

/**
Expand Down Expand Up @@ -136,12 +142,31 @@ public function pushToPipeline(ProcessTerminated $event = null): void
if ($event && $this->chunkSize->isChunked()) {
$process = $event->getProcess();
if (!$process->isToBeRetried()) {
unlink($process->getFilename());
$this->chunkFile->deleteChunkFile($process);
}
}

while (! $this->queuedProcesses->isEmpty() && $this->pipelineCollection->hasEmptySlots()) {
$this->pipelineCollection->push($this->queuedProcesses->dequeue());
}
}

public function onShutdown(): void
{
$this->pipelineCollection->triggerProcessTermination();

if ($this->chunkSize->isChunked()) {
$processes = $this->pipelineCollection->getRunningProcesses();
foreach ($processes as $process) {
$this->chunkFile->deleteChunkFile($process);
}
do {
try {
$this->chunkFile->deleteChunkFile($this->queuedProcesses->dequeue());
} catch (RuntimeException $e) {
//pass
}
} while (! $this->queuedProcesses->isEmpty());
}
}
}
42 changes: 41 additions & 1 deletion tests/Functional/Runner/ChunkFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace Tests\Functional\Runner;

use Paraunit\Bin\Paraunit;
use Paraunit\Runner\ChunkFile;
use Paraunit\Runner\Runner;
use Tests\BaseIntegrationTestCase;

class ChunkFileTest extends BaseIntegrationTestCase
{
public function testChunkedAllGreen(): void
public function testChunkedAllStubsSuite(): void
{
$this->setOption('chunk-size', '2');
$this->loadContainer();
Expand Down Expand Up @@ -82,6 +83,45 @@ public function testChunkedAllGreen(): void
$this->assertStringContainsString('3x in RaisingDeprecationTestStub::testDeprecation from Tests\Stub', $outputText);
}

public function testChunkedSigIntHandling(): void
{
$chunkCount = 2;

$this->setOption('configuration', $this->getStubPath() . DIRECTORY_SEPARATOR . 'phpunit_for_sigint_stubs.xml');
$this->setTextFilter('TestStubSigInt.php');
$this->setOption('chunk-size', '2');
$this->loadContainer();

$output = $this->getConsoleOutput();

$this->assertEquals(0, $this->executeRunner(), $output->getOutput());

$outputText = $output->getOutput();
$this->assertStringNotContainsString('Coverage', $outputText);
$this->assertOutputOrder($output, [
'PARAUNIT',
Paraunit::getVersion(),
' 3',
'Execution time',
"Executed: $chunkCount chunks, 3 tests",
'Risky Outcome output:',
'2 chunks with RISKY OUTCOME:',
]);

$this->assertStringContainsString('Tests\Stub\TestBTestStubSigInt::testBrokenTest', $outputText);
$this->assertStringContainsString('Tests\Stub\TestCTestStubSigInt::testBrokenTest', $outputText);
$this->assertStringContainsString('This test did not perform any assertions', $outputText);

/** @var ChunkFile $chunkFileService */
$chunkFileService = $this->getService(ChunkFile::class);
$fileFullPath = $this->getConfigForStubs();
$this->assertFileExists($fileFullPath);
foreach (range(0, $chunkCount - 1) as $chunkNumber) {
$chunkFileName = $chunkFileService->getChunkFileName($fileFullPath, $chunkNumber);
$this->assertFileDoesNotExist($chunkFileName);
}
}

private function executeRunner(): int
{
/** @var Runner $runner */
Expand Down
19 changes: 19 additions & 0 deletions tests/Stub/TestATestStubSigInt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace Tests\Stub;

class TestATestStubSigInt extends BrokenTestBase implements BrokenTestInterface
{
public function testBrokenTest(): void
{
usleep(100000);

$chunkFileName = __DIR__ . DIRECTORY_SEPARATOR . 'phpunit_for_sigint_stubs_0.xml';

$this->assertFileExists($chunkFileName);
posix_kill(posix_getppid(), SIGINT);
$this->assertFileDoesNotExist($chunkFileName);
}
}
12 changes: 12 additions & 0 deletions tests/Stub/TestBTestStubSigInt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);

namespace Tests\Stub;

class TestBTestStubSigInt extends BrokenTestBase implements BrokenTestInterface
{
public function testBrokenTest(): void
{
usleep(1000000);
}
}
12 changes: 12 additions & 0 deletions tests/Stub/TestCTestStubSigInt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);

namespace Tests\Stub;

class TestCTestStubSigInt extends BrokenTestBase implements BrokenTestInterface
{
public function testBrokenTest(): void
{
usleep(1000000);
}
}
25 changes: 25 additions & 0 deletions tests/Stub/phpunit_for_sigint_stubs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/7.0/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="../../vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1"/>
<ini name="intl.default_locale" value="en"/>
<ini name="intl.error_level" value="0"/>
<ini name="memory_limit" value="-1"/>
</php>

<testsuites>
<testsuite name="stubs" >
<directory suffix="TestStubSigInt.php">./</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<file>StubbedParaunitProcess.php</file>
</whitelist>
</filter>
</phpunit>

0 comments on commit d77712f

Please sign in to comment.