setName('test-command-with-warning'); } public function doExecute(): int { StaticContainer::get(LoggerInterface::class)->warning('warn'); return self::SUCCESS; } } class TestCommandWithError extends ConsoleCommand { public function configure() { parent::configure(); $this->setName('test-command-with-error'); $this->addNoValueOption('no-error'); } public function doExecute(): int { if (!$this->getInput()->getOption('no-error')) { StaticContainer::get(LoggerInterface::class)->error('error'); } return self::SUCCESS; } } class TestCommandWithFatalError extends ConsoleCommand { public function configure() { parent::configure(); $this->setName('test-command-with-fatal-error'); } public function doExecute(): int { try { \Piwik\ErrorHandler::pushFatalErrorBreadcrumb(static::class); $this->executeImpl(); } finally { \Piwik\ErrorHandler::popFatalErrorBreadcrumb(); } return self::SUCCESS; } public function executeImpl() { try { \Piwik\ErrorHandler::pushFatalErrorBreadcrumb(static::class, []); $val = ""; while (true) { $val .= str_repeat("*", 1024 * 1024 * 1024); } } finally { \Piwik\ErrorHandler::popFatalErrorBreadcrumb(); } } } class TestCommandWithException extends ConsoleCommand { public function configure() { parent::configure(); $this->setName('test-command-with-exception'); } public function doExecute(): int { throw new \Exception('test error'); } } class TestCommandWithSubscribedSignals extends ConsoleCommand { public function configure() { parent::configure(); $this->setName('test-command-with-subscribed-signals'); } public function getSystemSignalsToHandle(): array { return [\SIGHUP]; } public function doExecute(): int { return !empty($this->getSubscribedSignals()) ? self::SUCCESS : self::FAILURE; } } /** * @group ConsoleTest */ class ConsoleTest extends ConsoleCommandTestCase { public function setUp(): void { parent::setUp(); $this->application->addCommands([ new TestCommandWithWarning(), new TestCommandWithError(), new TestCommandWithSubscribedSignals(), ]); StaticContainer::get(FailureLogMessageDetector::class)->reset(); } public function testConsoleReturnsCorrectExitCodeIfCommandEmitsWarning() { $exitCode = $this->applicationTester->run([ 'command' => 'test-command-with-warning', ]); $this->assertEquals(1, $exitCode); } public function testConsoleReturnsCorrectExitCodeIfCommandEmitsError() { $exitCode = $this->applicationTester->run([ 'command' => 'test-command-with-error', ]); $this->assertEquals(1, $exitCode); } public function testConsoleReturnsCorrectExitCodeIfCommandDoesNotEmitAnything() { $exitCode = $this->applicationTester->run([ 'command' => 'test-command-with-error', '--no-error' => true, ]); $this->assertEquals(0, $exitCode); } public function testConsoleHandlesFatalErrorsCorrectly() { $cliPhp = new CliPhp(); $php = $cliPhp->findPhpBinary(); $command = $php . " -i | grep 'memory_limit => -1'"; $output = shell_exec($command); if ($output == "memory_limit => -1 => -1\n") { $this->markTestSkipped("no memory limit in php-cli"); } $command = Fixture::getCliCommandBase(); $command .= ' test-command-with-fatal-error'; $command .= ' 2>&1'; $output = shell_exec($command); $output = $this->normalizeOutput($output); $expected = << 1, 'message' => 'Allowed memory size of X bytes exhausted (tried to allocate X bytes)', 'file' => '/tests/PHPUnit/System/ConsoleTest.php', 'line' => %d, 'backtrace' => ' on /tests/PHPUnit/System/ConsoleTest.php(%d) #0 /tests/PHPUnit/System/ConsoleTest.php(%d): Piwik\\\\Tests\\\\System\\\\TestCommandWithFatalError->executeImpl() #1 /core/Plugin/ConsoleCommand.php(%d): Piwik\\\\Tests\\\\System\\\\TestCommandWithFatalError->doExecute() ', )) END; if (PHP_MAJOR_VERSION < 8) { $expected = "#!/usr/bin/env php\n" . $expected; } $this->assertStringMatchesFormat($expected, $output); } public function testConsoleHandlesExceptionsCorrectly() { $command = Fixture::getCliCommandBase(); $command .= ' test-command-with-exception'; $command .= ' 2>&1'; $output = shell_exec($command); $output = $this->normalizeOutput($output); $expected = <<assertStringMatchesFormat($expected, $output); } /** * @dataProvider getSignalSubscriptionOnlyActiveWithFeatureFlagEnabledData */ public function testSignalSubscriptionOnlyActiveWithFeatureFlagEnabled( bool $enableFeatureFlag, int $expectedExitCode ): void { $config = Config::getInstance(); $featureFlag = new SystemSignals(); $featureFlagConfig = $featureFlag->getName() . '_feature'; if ($enableFeatureFlag) { $config->FeatureFlags = [$featureFlagConfig => 'enabled']; } else { $config->FeatureFlags = [$featureFlagConfig => 'disabled']; } $exitCode = $this->applicationTester->run([ 'command' => 'test-command-with-subscribed-signals', ]); $this->assertEquals($expectedExitCode, $exitCode); } public function getSignalSubscriptionOnlyActiveWithFeatureFlagEnabledData(): iterable { yield 'active' => [true, ConsoleCommand::SUCCESS]; yield 'not active' => [false, ConsoleCommand::FAILURE]; } public static function provideContainerConfigBeforeClass() { return [ 'log.handlers' => [\Piwik\DI::get(FailureLogMessageDetector::class)], LoggerInterface::class => \Piwik\DI::create(Logger::class) ->constructor('piwik', \Piwik\DI::get('log.handlers'), \Piwik\DI::get('log.processors')), 'observers.global' => \Piwik\DI::add([ ['Console.filterCommands', \Piwik\DI::value(function (&$commands) { $commands[] = TestCommandWithFatalError::class; $commands[] = TestCommandWithException::class; $commands[] = TestCommandWithSubscribedSignals::class; })], ['Request.dispatch', \Piwik\DI::value(function ($module, $action) { if ($module === 'CorePluginsAdmin' && $action === 'safemode') { print "*** IN SAFEMODE ***\n"; // will appear in output } })], ]), ]; } private function normalizeOutput($output) { $output = str_replace(PIWIK_INCLUDE_PATH, '', $output); $output = preg_replace('/[0-9]+ bytes/', 'X bytes', $output); return $output; } }