1
0
قرینه از https://github.com/matomo-org/matomo.git synced 2025-08-22 15:07:44 +00:00
Files
matomo/tests/PHPUnit/System/ConsoleTest.php
Stefan Giehl eff138b4c3 Implements system signal handling for archiving interruption / termination (#22487)
* Allow signal handling in console commands

* Stop archiving after receiving SIGINT/SIGTERM

* Add tests for basic core:archive signal handling

* Abort symfony archiving requests for SIGTERM

* Add tests for non-symfony core:archive climulti methods

* Add tests for core:archive sigint fallback if sigterm is not fully supported

* Add tests for signal handling during core:archive init

* Add tests for signal handling during core:archive scheduled tasks

* Stop scheduled tasks after receiving SIGINT/SIGTERM

* Add tests for signal handling during scheduled-tasks:run

* Wrap signal handling support behind feature flag

* Update CHANGELOG.md

---------

Co-authored-by: Marc Neudert <marc@innocraft.com>
2025-02-26 16:08:20 +01:00

299 خطوط
8.4 KiB
PHP

<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
namespace Piwik\Tests\System;
use Piwik\CliMulti\CliPhp;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugins\CoreConsole\FeatureFlags\SystemSignals;
use Piwik\Plugins\Monolog\Handler\FailureLogMessageDetector;
use Piwik\Tests\Framework\Fixture;
use Piwik\Log\LoggerInterface;
use Piwik\Log\Logger;
use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase;
class TestCommandWithWarning extends ConsoleCommand
{
public function configure()
{
parent::configure();
$this->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 = <<<END
Fatal error: Allowed memory size of X bytes exhausted (tried to allocate X bytes) in /tests/PHPUnit/System/ConsoleTest.php on line 87
*** IN SAFEMODE ***
Matomo encountered an error: Allowed memory size of X bytes exhausted (tried to allocate X bytes) (which lead to: Error: array (
'type' => 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 = <<<END
*** IN SAFEMODE ***
In ConsoleTest.php line %d:
\n test error \n \n
test-command-with-exception
END;
if (PHP_MAJOR_VERSION < 8) {
$expected = "#!/usr/bin/env php\n" . $expected;
}
$this->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;
}
}