Skip to content

Add app tear down process for error states #212

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

Merged
merged 2 commits into from
May 21, 2021
Merged
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
19 changes: 5 additions & 14 deletions app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,28 +384,19 @@ function (CgiResult $result) use ($c) {
],
],
'code-patcher' => [
'cli.verify.start' => [
containerListener(CodePatchListener::class, 'patch'),
],
'cli.verify.finish' => [
containerListener(CodePatchListener::class, 'revert'),
],
'cli.run.start' => [
'verify.pre.execute' => [
containerListener(CodePatchListener::class, 'patch'),
],
'cli.run.finish' => [
'verify.post.execute' => [
containerListener(CodePatchListener::class, 'revert'),
],
'cgi.verify.start' => [
'run.start' => [
containerListener(CodePatchListener::class, 'patch'),
],
'cgi.verify.finish' => [
'run.finish' => [
containerListener(CodePatchListener::class, 'revert'),
],
'cgi.run.start' => [
containerListener(CodePatchListener::class, 'patch'),
],
'cgi.run.finish' => [
'application.tear-down' => [
containerListener(CodePatchListener::class, 'revert'),
],
],
Expand Down
31 changes: 31 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
use DI\Container;
use DI\ContainerBuilder;
use PhpSchool\PhpWorkshop\Check\CheckRepository;
use PhpSchool\PhpWorkshop\Event\Event;
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
use PhpSchool\PhpWorkshop\Exception\MissingArgumentException;
use PhpSchool\PhpWorkshop\Factory\ResultRendererFactory;
use PhpSchool\PhpWorkshop\Output\OutputInterface;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;

use function class_exists;
Expand Down Expand Up @@ -69,6 +72,11 @@ final class Application
*/
private $frameworkConfigLocation = __DIR__ . '/../app/config.php';

/**
* @var ?ContainerInterface
*/
private $container;

/**
* It should be instantiated with the title of
* the workshop and the path to the DI configuration file.
Expand Down Expand Up @@ -163,6 +171,10 @@ public function setBgColour(string $colour): void

public function configure(): ContainerInterface
{
if ($this->container instanceof ContainerInterface) {
return $this->container;
}

$container = $this->getContainer();

foreach ($this->exercises as $exercise) {
Expand Down Expand Up @@ -192,6 +204,12 @@ public function configure(): ContainerInterface
}
}

set_error_handler(function () use ($container): bool {
$this->tearDown($container);
return false; // Use default error handler
});

$this->container = $container;
return $container;
}

Expand Down Expand Up @@ -227,6 +245,8 @@ public function run(): int
$message = str_replace($basePath, '', $message);
}

$this->tearDown($container);

$container
->get(OutputInterface::class)
->printError(
Expand Down Expand Up @@ -268,4 +288,15 @@ private function getContainer(): Container

return $containerBuilder->build();
}

private function tearDown(ContainerInterface $container): void
{
try {
$container
->get(EventDispatcher::class)
->dispatch(new Event('application.tear-down'));
} catch (\Throwable $t) {
$container->get(LoggerInterface::class)->error($t->getMessage(), ['exception' => $t]);
}
}
}
2 changes: 1 addition & 1 deletion src/CommandRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function __construct(
/**
* @param CommandDefinition $c
*/
private function addCommand(CommandDefinition $c): void
public function addCommand(CommandDefinition $c): void
{
if (isset($this->commands[$c->getName()])) {
throw new InvalidArgumentException(sprintf('Command with name: "%s" already exists', $c->getName()));
Expand Down
43 changes: 43 additions & 0 deletions src/Output/NullOutput.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PhpWorkshop\Output;

class NullOutput implements OutputInterface
{
public function printError(string $error): void
{
// noop
}

public function write(string $content): void
{
// noop
}

public function writeLines(array $lines): void
{
// noop
}

public function writeLine(string $line): void
{
// noop
}

public function emptyLine(): void
{
// noop
}

public function lineBreak(): void
{
// noop
}

public function writeTitle(string $title): void
{
// noop
}
}
131 changes: 106 additions & 25 deletions test/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,44 @@
namespace PhpSchool\PhpWorkshopTest;

use PhpSchool\PhpWorkshop\Application;
use PHPUnit\Framework\TestCase;
use PhpSchool\PhpWorkshop\CommandDefinition;
use PhpSchool\PhpWorkshop\CommandRouter;
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;

class ApplicationTest extends TestCase
use PhpSchool\PhpWorkshop\Exception\RuntimeException;
use PhpSchool\PhpWorkshop\Output\NullOutput;
use PhpSchool\PhpWorkshop\Output\OutputInterface;
use PhpSchool\PhpWorkshopTest\Asset\MockEventDispatcher;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class ApplicationTest extends BaseTest
{
public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void
{
$frameworkFileContent = <<<'FRAME'
<?php return [
'eventListeners' => [
'event1' => [
'entry1',
'entry2',
]
]
];
FRAME;

$localFileContent = <<<'LOCAL'
<?php return [
'eventListeners' => [
'event1' => [
'entry3',
]
]
];
LOCAL;

$frameworkFileContent = '<?php return [';
$frameworkFileContent .= " 'eventListeners' => [";
$frameworkFileContent .= " 'event1' => [";
$frameworkFileContent .= " 'entry1',";
$frameworkFileContent .= " 'entry2',";
$frameworkFileContent .= ' ]';
$frameworkFileContent .= ' ]';
$frameworkFileContent .= '];';

$localFileContent = '<?php return [';
$localFileContent .= " 'eventListeners' => [";
$localFileContent .= " 'event1' => [";
$localFileContent .= " 'entry3',";
$localFileContent .= ' ]';
$localFileContent .= ' ]';
$localFileContent .= '];';

$localFile = sprintf('%s/%s', sys_get_temp_dir(), uniqid($this->getName(), true));
$frameworkFile = sprintf('%s/%s', sys_get_temp_dir(), uniqid($this->getName(), true));
file_put_contents($frameworkFile, $frameworkFileContent);
file_put_contents($localFile, $localFileContent);
$localFile = $this->getTemporaryFile(uniqid($this->getName(), true), $localFileContent);
$frameworkFile = $this->getTemporaryFile(uniqid($this->getName(), true), $frameworkFileContent);

$app = new Application('Test App', $localFile);

Expand All @@ -48,7 +57,7 @@ public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void

$eventListeners = $container->get('eventListeners');

$this->assertEquals(
self::assertEquals(
[
'event1' => [
'entry1',
Expand Down Expand Up @@ -85,4 +94,76 @@ public function testExceptionIsThrownIfResultRendererClassDoesNotExist(): void
$app = new Application('My workshop', __DIR__ . '/../app/config.php');
$app->addResult(\PhpSchool\PhpWorkshop\Result\Success::class, \NotExistingClass::class);
}

public function testTearDownEventIsFiredOnApplicationException(): void
{
$configFile = $this->getTemporaryFile('config.php', '<?php return [];');
$application = new Application('Testing TearDown', $configFile);

$container = $application->configure();
$container->set('basePath', __DIR__);
$container->set(EventDispatcher::class, new MockEventDispatcher());
$container->set(OutputInterface::class, new NullOutput());

/** @var MockEventDispatcher $eventDispatcher */
$eventDispatcher = $container->get(EventDispatcher::class);

$commandRouter = $container->get(CommandRouter::class);
$commandRouter->addCommand(new CommandDefinition('Failure', [], function () {
throw new RuntimeException('We failed somewhere...');
}));

$_SERVER['argv'] = [$this->getName(), 'Failure'];

$application->run();

self::assertSame(1, $eventDispatcher->getEventDispatchCount('application.tear-down'));
}

public function testLoggingExceptionDuringTearDown(): void
{
$configFile = $this->getTemporaryFile('config.php', '<?php return [];');
$application = new Application('Testing tear down logging', $configFile);
$exception = new \Exception('Unexpected error');

$container = $application->configure();
$container->set('basePath', __DIR__);
$container->set(OutputInterface::class, new NullOutput());
$container->set(LoggerInterface::class, new MockLogger());
$container->set('eventListeners', [
'testing-failure-logging' => [
'application.tear-down' => [
static function () use ($exception) {
throw $exception;
},
]
]
]);

$commandRouter = $container->get(CommandRouter::class);
$commandRouter->addCommand(new CommandDefinition('Failure', [], function () {
throw new RuntimeException('We failed somewhere...');
}));

$application->run();

/** @var MockLogger $logger */
$logger = $container->get(LoggerInterface::class);
self::assertCount(1, $logger->messages);
self::assertSame('Unexpected error', $logger->messages[0]['message']);
self::assertSame($exception, $logger->messages[0]['context']['exception']);
}

public function testConfigureReturnsSameContainerInstance(): void
{
$configFile = $this->getTemporaryFile('config.php', '<?php return [];');
$application = new Application('Testing Configure', $configFile);

self::assertSame($application->configure(), $application->configure());
}

public function tearDown(): void
{
parent::tearDown();
}
}
51 changes: 51 additions & 0 deletions test/Asset/MockEventDispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

namespace PhpSchool\PhpWorkshopTest\Asset;

use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\Event\EventInterface;

class MockEventDispatcher extends EventDispatcher
{
private $dispatches = [];
private $listeners = [];

public function __construct()
{
// noop
}

public function dispatch(EventInterface $event): EventInterface
{
isset($this->dispatches[$event->getName()])
? $this->dispatches[$event->getName()]++
: $this->dispatches[$event->getName()] = 1;

return $event;
}

public function listen($eventNames, callable $callback): void
{
if (!is_array($eventNames)) {
$eventNames = [$eventNames];
}

foreach ($eventNames as $eventName) {
isset($this->listeners[$eventName])
? $this->listeners[$eventName][] = $callback
: $this->listeners[$eventName] = [$callback];
}
}

public function getEventDispatchCount(string $eventName): int
{
return $this->dispatches[$eventName] ?? 0;
}

public function getEventListeners(string $eventName): array
{
return $this->listeners[$eventName] ?? [];
}
}
Loading