diff --git a/app/config.php b/app/config.php index adcfbb33..5cf7fe65 100644 --- a/app/config.php +++ b/app/config.php @@ -4,6 +4,7 @@ use Colors\Color; use PhpSchool\PhpWorkshop\Listener\InitialCodeListener; +use PhpSchool\PhpWorkshop\Logger\ConsoleLogger; use PhpSchool\PhpWorkshop\Logger\Logger; use Psr\Log\LoggerInterface; use function DI\create; @@ -96,6 +97,10 @@ $appName = $c->get('appName'); $globalDir = $c->get('phpschoolGlobalDir'); + if ($c->get('debugMode')) { + return new ConsoleLogger($c->get(OutputInterface::class), $c->get(Color::class)); + } + return new Logger("$globalDir/logs/$appName.log"); }, ExerciseDispatcher::class => function (ContainerInterface $c) { diff --git a/composer.json b/composer.json index c78f1610..86d65678 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,8 @@ "php-school/keylighter": "^0.8.4", "nikic/php-parser": "^4.0", "guzzlehttp/guzzle": "^7.2", - "psr/log": "^1.1" + "psr/log": "^1.1", + "ext-json": "*" }, "require-dev": { "composer/composer": "^2.0", diff --git a/src/Application.php b/src/Application.php index ee7978d3..71086e53 100644 --- a/src/Application.php +++ b/src/Application.php @@ -169,13 +169,13 @@ public function setBgColour(string $colour): void $this->bgColour = $colour; } - public function configure(): ContainerInterface + public function configure(bool $debugMode = false): ContainerInterface { if ($this->container instanceof ContainerInterface) { return $this->container; } - $container = $this->getContainer(); + $container = $this->getContainer($debugMode); foreach ($this->exercises as $exercise) { if (false === $container->has($exercise)) { @@ -221,10 +221,20 @@ public function configure(): ContainerInterface */ public function run(): int { - $container = $this->configure(); + $args = $_SERVER['argv'] ?? []; + + $debug = any($args, function (string $arg) { + return $arg === '--debug'; + }); + + $args = array_values(array_filter($args, function (string $arg) { + return $arg !== '--debug'; + })); + + $container = $this->configure($debug); try { - $exitCode = $container->get(CommandRouter::class)->route(); + $exitCode = $container->get(CommandRouter::class)->route($args); } catch (MissingArgumentException $e) { $container ->get(OutputInterface::class) @@ -261,9 +271,10 @@ public function run(): int } /** + * @param bool $debugMode * @return Container */ - private function getContainer(): Container + private function getContainer(bool $debugMode): Container { $containerBuilder = new ContainerBuilder(); $containerBuilder->addDefinitions( @@ -276,6 +287,7 @@ private function getContainer(): Container $containerBuilder->addDefinitions( [ 'workshopTitle' => $this->workshopTitle, + 'debugMode' => $debugMode, 'exercises' => $this->exercises, 'workshopLogo' => $this->logo, 'bgColour' => $this->bgColour, diff --git a/src/CommandRouter.php b/src/CommandRouter.php index 98f8ec74..9043282c 100644 --- a/src/CommandRouter.php +++ b/src/CommandRouter.php @@ -100,7 +100,6 @@ public function addCommand(CommandDefinition $c): void */ public function route(array $args = null): int { - if (null === $args) { $args = $_SERVER['argv'] ?? []; } diff --git a/src/ExerciseRenderer.php b/src/ExerciseRenderer.php index 09861da5..44d59fc0 100644 --- a/src/ExerciseRenderer.php +++ b/src/ExerciseRenderer.php @@ -82,8 +82,6 @@ public function __construct( */ public function __invoke(CliMenu $menu): void { - $menu->close(); - $item = $menu->getSelectedItem(); $exercise = $this->exerciseRepository->findByName($item->getText()); $exercises = $this->exerciseRepository->findAll(); diff --git a/src/Factory/MenuFactory.php b/src/Factory/MenuFactory.php index 137ea4d9..30605ae5 100644 --- a/src/Factory/MenuFactory.php +++ b/src/Factory/MenuFactory.php @@ -59,6 +59,7 @@ public function __invoke(ContainerInterface $c): CliMenu $builder->addItem( $exercise->getName(), function (CliMenu $menu) use ($exerciseRenderer, $eventDispatcher, $exercise) { + $menu->close(); $this->dispatchExerciseSelectedEvent($eventDispatcher, $exercise); $exerciseRenderer->__invoke($menu); }, diff --git a/src/Logger/ConsoleLogger.php b/src/Logger/ConsoleLogger.php new file mode 100644 index 00000000..b491e28a --- /dev/null +++ b/src/Logger/ConsoleLogger.php @@ -0,0 +1,44 @@ +output = $output; + $this->color = $color; + } + + public function log($level, $message, array $context = []): void + { + $parts = [ + sprintf( + '%s - %s - %s', + $this->color->fg('yellow', (new \DateTime())->format('H:i:s')), + $this->color->bg('red', strtoupper($level)), + $this->color->fg('red', $message) + ), + json_encode($context, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) + ]; + + $this->output->writeLine(implode("\n", $parts)); + } +} diff --git a/src/functions.php b/src/functions.php index 1e1a8ec9..b55a5279 100644 --- a/src/functions.php +++ b/src/functions.php @@ -73,3 +73,19 @@ function collect(array $array): Collection return new Collection($array); } } + + +if (!function_exists('any')) { + + /** + * @param array $values + * @param callable $cb + * @return bool + */ + function any(array $values, callable $cb): bool + { + return array_reduce($values, function (bool $carry, $value) use ($cb) { + return $carry || $cb($value); + }, false); + } +} diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index f4cf3160..79ea9e1f 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -15,6 +15,9 @@ use PhpSchool\PhpWorkshopTest\Asset\MockEventDispatcher; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; +use PhpSchool\PhpWorkshop\Logger\ConsoleLogger; +use PhpSchool\PhpWorkshop\Logger\Logger; +use PHPUnit\Framework\TestCase; class ApplicationTest extends BaseTest { @@ -53,7 +56,7 @@ public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void $rp->setAccessible(true); $rp->setValue($app, $frameworkFile); - $container = $rm->invoke($app); + $container = $rm->invoke($app, false); $eventListeners = $container->get('eventListeners'); @@ -162,8 +165,16 @@ public function testConfigureReturnsSameContainerInstance(): void self::assertSame($application->configure(), $application->configure()); } - public function tearDown(): void + public function testDebugFlagSwitchesLoggerToConsoleLogger(): void { - parent::tearDown(); + $configFile = $this->getTemporaryFile('config.php', 'configure(true); + + $container->set('phpschoolGlobalDir', $this->getTemporaryDirectory()); + $container->set('appName', 'my-workshop'); + + $logger = $container->get(LoggerInterface::class); + self::assertInstanceOf(ConsoleLogger::class, $logger); } } diff --git a/test/BaseTest.php b/test/BaseTest.php index 426b12ec..f7fe6cc0 100644 --- a/test/BaseTest.php +++ b/test/BaseTest.php @@ -16,10 +16,8 @@ abstract class BaseTest extends TestCase public function getTemporaryDirectory(): string { if (!$this->tempDirectory) { - $tempDirectory = System::tempDir($this->getName()); - mkdir($tempDirectory, 0777, true); - - $this->tempDirectory = realpath($tempDirectory); + $this->tempDirectory = System::tempDir($this->getName()); + mkdir($this->tempDirectory, 0777, true); } return $this->tempDirectory; diff --git a/test/ExerciseRendererTest.php b/test/ExerciseRendererTest.php index 94d1b9df..d15464c0 100644 --- a/test/ExerciseRendererTest.php +++ b/test/ExerciseRendererTest.php @@ -33,10 +33,6 @@ public function testExerciseRendererSetsCurrentExerciseAndRendersExercise(): voi ->method('getSelectedItem') ->willReturn($item); - $menu - ->expects($this->once()) - ->method('close'); - $exercise1 = $this->createMock(ExerciseInterface::class); $exercise2 = $this->createMock(ExerciseInterface::class); $exercises = [$exercise1, $exercise2]; diff --git a/test/Factory/MenuFactoryTest.php b/test/Factory/MenuFactoryTest.php index 117f92b2..3a09e0c2 100644 --- a/test/Factory/MenuFactoryTest.php +++ b/test/Factory/MenuFactoryTest.php @@ -2,6 +2,8 @@ namespace PhpSchool\PhpWorkshopTest\Factory; +use PhpSchool\CliMenu\MenuItem\SelectableItem; +use PhpSchool\PhpWorkshop\Event\EventInterface; use Psr\Container\ContainerInterface; use PhpSchool\CliMenu\CliMenu; use PhpSchool\PhpWorkshop\Command\CreditsCommand; @@ -68,6 +70,84 @@ public function testFactoryReturnsInstance(): void $factory = new MenuFactory(); - $this->assertInstanceOf(CliMenu::class, $factory($container)); + + $factory($container); + } + + public function testSelectExercise(): void + { + $container = $this->createMock(ContainerInterface::class); + $userStateSerializer = $this->createMock(UserStateSerializer::class); + $userStateSerializer + ->expects($this->once()) + ->method('deSerialize') + ->willReturn(new UserState()); + + $exerciseRepository = $this->createMock(ExerciseRepository::class); + $exercise = $this->createMock(ExerciseInterface::class); + $exercise + ->method('getName') + ->willReturn('Exercise'); + $exerciseRepository + ->expects($this->once()) + ->method('findAll') + ->willReturn([$exercise]); + + $terminal = $this->createMock(Terminal::class); + $terminal + ->method('getWidth') + ->willReturn(70); + + $eventDispatcher = $this->createMock(EventDispatcher::class); + $eventDispatcher + ->expects(self::exactly(2)) + ->method('dispatch') + ->withConsecutive( + [ + self::callback(function ($event) { + return $event instanceof EventInterface && $event->getName() === 'exercise.selected'; + }) + ], + [ + self::callback(function ($event) { + return $event instanceof EventInterface && $event->getName() === 'exercise.selected.exercise'; + }) + ] + ); + + $exerciseRenderer = $this->createMock(ExerciseRenderer::class); + $exerciseRenderer->expects(self::once()) + ->method('__invoke') + ->with(self::isInstanceOf(CliMenu::class)); + + $services = [ + UserStateSerializer::class => $userStateSerializer, + ExerciseRepository::class => $exerciseRepository, + ExerciseRenderer::class => $exerciseRenderer, + HelpCommand::class => $this->createMock(HelpCommand::class), + CreditsCommand::class => $this->createMock(CreditsCommand::class), + ResetProgress::class => $this->createMock(ResetProgress::class), + 'workshopLogo' => 'LOGO', + 'bgColour' => 'black', + 'fgColour' => 'green', + 'workshopTitle' => 'TITLE', + WorkshopType::class => WorkshopType::STANDARD(), + EventDispatcher::class => $eventDispatcher, + Terminal::class => $terminal + ]; + + $container + ->method('get') + ->willReturnCallback(function ($name) use ($services) { + return $services[$name]; + }); + + + $factory = new MenuFactory(); + + $menu = $factory($container); + + $firstExercise = $menu->getItemByIndex(6); + $menu->executeAsSelected($firstExercise); } } diff --git a/test/FunctionsTest.php b/test/FunctionsTest.php index 70055e31..cff066a1 100644 --- a/test/FunctionsTest.php +++ b/test/FunctionsTest.php @@ -40,4 +40,15 @@ public function camelCaseToKebabCaseProvider(): array ] ]; } + + public function testAny(): void + { + self::assertEquals(true, any([1, 2, 3, 10, 11], function (int $num) { + return $num > 10; + })); + + self::assertEquals(false, any([1, 2, 3, 10, 11], function (int $num) { + return $num > 11; + })); + } } diff --git a/test/Logger/ConsoleLoggerTest.php b/test/Logger/ConsoleLoggerTest.php new file mode 100644 index 00000000..7d991a1a --- /dev/null +++ b/test/Logger/ConsoleLoggerTest.php @@ -0,0 +1,37 @@ +container->set('phpschoolGlobalDir', $this->getTemporaryDirectory()); + $this->container->set('appName', 'my-workshop'); + $this->container->set('debugMode', true); + } + + public function testConsoleLoggerIsCreatedIfDebugModeEnable(): void + { + $this->assertInstanceOf(ConsoleLogger::class, $this->container->get(LoggerInterface::class)); + } + + public function testLoggerWithContext(): void + { + $logger = $this->container->get(LoggerInterface::class); + $logger->critical('Failed to copy file', ['exercise' => 'my-exercise']); + + $out = StringUtil::stripAnsiEscapeSequence($this->getActualOutputForAssertion()); + + $match = '/\d{2}\:\d{2}\:\d{2} - CRITICAL - Failed to copy file\n{\n "exercise": "my-exercise"\n}/'; + $this->assertMatchesRegularExpression($match, $out); + } +} diff --git a/test/Logger/LoggerTest.php b/test/Logger/LoggerTest.php index cdfc26b9..8854de1b 100644 --- a/test/Logger/LoggerTest.php +++ b/test/Logger/LoggerTest.php @@ -15,6 +15,7 @@ public function setUp(): void $this->container->set('phpschoolGlobalDir', $this->getTemporaryDirectory()); $this->container->set('appName', 'my-workshop'); + $this->container->set('debugMode', false); } public function testLoggerDoesNotCreateFileIfNoMessageIsLogged(): void