diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 5f978ed..3160eaa 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -11,6 +11,8 @@ env: APP_ENV: prod SALES_CHANNEL_URL: http://localhost:8000 DATABASE_URL: mysql://root@127.0.0.1/shopware + # Disable our own telemetry to not fill test runs + DO_NOT_TRACK: 1 jobs: installation: @@ -37,10 +39,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: ./custom/plugins/deployment-helper + path: ./custom/static-plugins/deployment-helper - name: Set fake version into deployment helper - run: composer -d custom/plugins/deployment-helper config version 999.9.9 + run: composer -d custom/static-plugins/deployment-helper config version 999.9.9 - name: Install Deployment Helper run: composer require --dev 'shopware/deployment-helper:*' @@ -86,10 +88,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: ./custom/plugins/deployment-helper + path: ./custom/static-plugins/deployment-helper - name: Set fake version into deployment helper - run: composer -d custom/plugins/deployment-helper config version 999.9.9 + run: composer -d custom/static-plugins/deployment-helper config version 999.9.9 - name: Install Deployment Helper run: composer require --dev 'shopware/deployment-helper:*' @@ -146,10 +148,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 with: - path: ./custom/plugins/deployment-helper + path: ./custom/static-plugins/deployment-helper - name: Set fake version into deployment helper - run: composer -d custom/plugins/deployment-helper config version 999.9.9 + run: composer -d custom/static-plugins/deployment-helper config version 999.9.9 - name: Install Deployment Helper run: composer require --dev 'shopware/deployment-helper:*' diff --git a/.gitignore b/.gitignore index 858b804..201a14f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ /.idea +/.claude /vendor /composer.lock /var/cache /build/coverage +/coverage +/.junie diff --git a/bin/shopware-deployment-helper b/bin/shopware-deployment-helper index 6b49ca0..696bc69 100755 --- a/bin/shopware-deployment-helper +++ b/bin/shopware-deployment-helper @@ -17,7 +17,7 @@ $includables = [ __DIR__ . '/vendor/autoload.php', ]; -if (str_contains(__DIR__, 'custom/plugins')) { +if (str_contains(__DIR__, 'custom/plugins') || str_contains(__DIR__, 'custom/static-plugins')) { $includables[] = __DIR__ . '/../../../../vendor/autoload.php'; } diff --git a/composer.json b/composer.json index 644a373..31db51b 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "php": ">=8.2", "ext-dom": "*", "ext-pdo": "*", + "ext-sockets": "*", "digilist/dependency-graph": ">=0.4.1", "doctrine/dbal": "^3.0 || ^4.0", "symfony/config": "^7.0 || ^6.0", diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 0eecf11..78fd460 100644 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -9,6 +9,7 @@ use Shopware\Deployment\Services\HookExecutor; use Shopware\Deployment\Services\InstallationManager; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Services\UpgradeManager; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Attribute\AsCommand; @@ -27,6 +28,7 @@ public function __construct( private readonly UpgradeManager $upgradeManager, private readonly HookExecutor $hookExecutor, private readonly EventDispatcherInterface $eventDispatcher, + private readonly TrackingService $trackingService, ) { parent::__construct(); } @@ -41,6 +43,7 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { + $installed = $this->state->isInstalled(); $timeout = $input->getOption('timeout'); $config = new RunConfiguration( @@ -50,18 +53,26 @@ protected function execute(InputInterface $input, OutputInterface $output): int forceReinstallation: EnvironmentHelper::getVariable('SHOPWARE_DEPLOYMENT_FORCE_REINSTALL', '0') === '1', ); - $installed = $this->state->isInstalled(); - if ($config->forceReinstallation && $this->state->getPreviousVersion() === 'unknown') { $installed = false; } $this->hookExecutor->execute(HookExecutor::HOOK_PRE); - if ($installed) { - $this->upgradeManager->run($config, $output); - } else { - $this->installationManager->run($config, $output); + try { + if ($installed) { + $this->upgradeManager->run($config, $output); + } else { + $this->installationManager->run($config, $output); + } + } finally { + $this->trackingService->track('php_version', [ + 'php_version' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, + ]); + + $this->trackingService->track('mysql_version', [ + 'mysql_version' => $this->state->getMySqlVersion(), + ]); } $this->eventDispatcher->dispatch(new PostDeploy($config, $output)); diff --git a/src/Helper/ConsoleOutputInterface.php b/src/Helper/ConsoleOutputInterface.php new file mode 100644 index 0000000..141d5af --- /dev/null +++ b/src/Helper/ConsoleOutputInterface.php @@ -0,0 +1,12 @@ +timeout = $this->validateTimeout($timeout ?? (float) EnvironmentHelper::getVariable('SHOPWARE_DEPLOYMENT_TIMEOUT', '60')); } @@ -32,17 +33,11 @@ public function shell(array $command): void $startTime = $this->printPreStart($command); - if (\function_exists('stream_isatty') && stream_isatty(\STDOUT)) { + if ($this->output instanceof DefaultConsoleOutput && \function_exists('stream_isatty') && stream_isatty(\STDOUT)) { $process->setTty(true); } - $process->run(function (string $type, string $buffer): void { - if ($type === Process::ERR) { - fwrite(\STDERR, $buffer); - } else { - fwrite(\STDOUT, $buffer); - } - }); + $process->run($this->output(...)); if (!$process->isSuccessful()) { throw new \RuntimeException('Execution of ' . implode(' ', $command) . ' failed'); @@ -63,17 +58,11 @@ public function run(array $args): void $startTime = $this->printPreStart($completeCmd); - if (\function_exists('stream_isatty') && stream_isatty(\STDOUT)) { + if ($this->output instanceof DefaultConsoleOutput && \function_exists('stream_isatty') && stream_isatty(\STDOUT)) { $process->setTty(true); } - $process->run(function (string $type, string $buffer): void { - if ($type === Process::ERR) { - fwrite(\STDERR, $buffer); - } else { - fwrite(\STDOUT, $buffer); - } - }); + $process->run($this->output(...)); if (!$process->isSuccessful()) { throw new \RuntimeException('Execution of ' . implode(' ', $args) . ' failed'); @@ -100,17 +89,11 @@ public function runAndTail(string $code): void $process = new Process(['sh', '-c', $code], $this->projectDir); $process->setTimeout($this->timeout); - if (\function_exists('stream_isatty') && stream_isatty(\STDOUT)) { + if ($this->output instanceof DefaultConsoleOutput && \function_exists('stream_isatty') && stream_isatty(\STDOUT)) { $process->setTty(true); } - $process->run(function (string $type, string $buffer): void { - if ($type === Process::ERR) { - fwrite(\STDERR, $buffer); - } else { - fwrite(\STDOUT, $buffer); - } - }); + $process->run($this->output(...)); if (!$process->isSuccessful()) { throw new \RuntimeException('Execution of ' . $originalCode . ' failed'); @@ -140,6 +123,15 @@ public function setTimeout(?float $timeout): static return $this; } + private function output(string $type, string $buffer): void + { + if ($type === Process::ERR) { + $this->output->writeStderr($buffer); + } else { + $this->output->writeStdout($buffer); + } + } + /** * @param array $cmd */ @@ -148,14 +140,14 @@ private function printPreStart(array $cmd): float $cmdString = implode(' ', $cmd); $startTime = microtime(true); - fwrite(\STDOUT, \PHP_EOL); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, "============== [deployment-helper] ==============\n"); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \sprintf("Start: %s\n", $cmdString)); - fwrite(\STDOUT, \sprintf("Time limit: %s seconds\n", $this->timeout)); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \PHP_EOL); + $this->output->writeStdout(\PHP_EOL); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout("============== [deployment-helper] ==============\n"); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\sprintf("Start: %s\n", $cmdString)); + $this->output->writeStdout(\sprintf("Time limit: %s seconds\n", $this->timeout)); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\PHP_EOL); return $startTime; } @@ -165,17 +157,17 @@ private function printPreStart(array $cmd): float */ private function printPostStart(array $cmd, float $startTime): void { - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, "============== [deployment-helper] ==============\n"); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \sprintf("End: %s\n", implode(' ', $cmd))); - fwrite(\STDOUT, \sprintf( + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout("============== [deployment-helper] ==============\n"); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\sprintf("End: %s\n", implode(' ', $cmd))); + $this->output->writeStdout(\sprintf( "> Time: %sms\n", number_format((microtime(true) - $startTime) * 1000, 2, '.', ''), )); - fwrite(\STDOUT, "=================================================\n"); - fwrite(\STDOUT, \PHP_EOL); + $this->output->writeStdout("=================================================\n"); + $this->output->writeStdout(\PHP_EOL); } /** diff --git a/src/Services/InstallationManager.php b/src/Services/InstallationManager.php index 6a7ad18..4aeeb1a 100644 --- a/src/Services/InstallationManager.php +++ b/src/Services/InstallationManager.php @@ -24,6 +24,7 @@ public function __construct( private readonly HookExecutor $hookExecutor, private readonly ProjectConfiguration $configuration, private readonly AccountService $accountService, + private readonly TrackingService $trackingService, ) { } @@ -56,7 +57,12 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $additionalInstallParameters[] = '--drop-database'; } + $took = microtime(true); $this->processHelper->console(['system:install', '--create-database', '--shop-locale=' . $shopLocale, '--shop-currency=' . $shopCurrency, '--force', ...$additionalInstallParameters]); + + $this->trackingService->persistId(); + $this->trackingService->track('installed', ['took' => microtime(true) - $took, 'shopware_version' => $this->state->getCurrentVersion()]); + $this->processHelper->console(['user:create', $adminUser, '--password=' . $adminPassword]); $this->processHelper->console(['messenger:setup-transports']); diff --git a/src/Services/ShopwareState.php b/src/Services/ShopwareState.php index e8628bb..3425830 100644 --- a/src/Services/ShopwareState.php +++ b/src/Services/ShopwareState.php @@ -100,4 +100,50 @@ public function disableMaintenanceMode(): void $this->connection->executeStatement('UPDATE sales_channel SET maintenance = ? WHERE id = UNHEX(?)', [$maintenance, $id]); } } + + public function getMySqlVersion(): string + { + $version = $this->extractMySQLVersion($this->connection->fetchOne('SELECT VERSION()')); + + if (isset($version['mariadb'])) { + return 'mariadb-' . $version['mariadb']; + } + + if (isset($version['mysql'])) { + return 'mysql-' . $version['mysql']; + } + + return 'unknown'; + } + + /** + * @return array{mysql?: string, mariadb?: string} + */ + private function extractMySQLVersion(string $versionString): array + { + if (stripos($versionString, 'mariadb') === false) { + $pos = strpos($versionString, '-'); + if (\is_int($pos)) { + $versionString = substr($versionString, 0, $pos); + } + + return ['mysql' => $versionString]; + } + + return ['mariadb' => self::getVersionNumber($versionString)]; + } + + private static function getVersionNumber(string $versionString): string + { + $match = preg_match( + '/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', + $versionString, + $versionParts, + ); + if ($match === 0 || \is_bool($match)) { + throw new \RuntimeException(\sprintf('Invalid version string: %s', $versionString)); + } + + return $versionParts['major'] . '.' . $versionParts['minor']; + } } diff --git a/src/Services/TrackingService.php b/src/Services/TrackingService.php new file mode 100644 index 0000000..016fce3 --- /dev/null +++ b/src/Services/TrackingService.php @@ -0,0 +1,102 @@ + + */ + private array $defaultTags; + + private string $id; + + private \Socket|false $socket; + + private string $domain; + + public function __construct( + private readonly SystemConfigHelper $systemConfigHelper, + private readonly ShopwareState $shopwareState, + ) { + $this->socket = @socket_create(\AF_INET, \SOCK_DGRAM, \SOL_UDP); + $this->domain = EnvironmentHelper::getVariable('SHOPWARE_TRACKING_DOMAIN', self::DEFAULT_TRACKING_DOMAIN); + } + + /** + * @param array $tags + */ + public function track(string $eventName, array $tags = []): void + { + if (EnvironmentHelper::hasVariable('DO_NOT_TRACK')) { + return; + } + + $tags += $this->getTags(); + $id = $this->getId(); + + if ($this->socket === false) { + return; + } + + $payload = json_encode([ + 'event' => 'deployment_helper.' . $eventName, + 'tags' => $tags, + 'user_id' => $id, + 'timestamp' => (new \DateTime())->format(\DateTimeInterface::ATOM), + ], \JSON_THROW_ON_ERROR); + + @socket_sendto($this->socket, $payload, \strlen($payload), 0, $this->domain, 9000); + } + + public function persistId(): void + { + if (isset($this->id)) { + $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $this->id); + } + } + + /** + * @return array + */ + private function getTags(): array + { + if (isset($this->defaultTags)) { + return $this->defaultTags; + } + + $this->defaultTags = [ + 'shopware_version' => $this->shopwareState->getCurrentVersion(), + ]; + + return $this->defaultTags; + } + + private function getId(): string + { + if (isset($this->id)) { + return $this->id; + } + + try { + $id = $this->systemConfigHelper->get(self::DEPLOYMENT_HELPER_ID); + } catch (\Throwable) { + $this->id = $id = bin2hex(random_bytes(16)); + } + + if ($id === null) { + $this->id = $id = bin2hex(random_bytes(16)); + $this->systemConfigHelper->set(self::DEPLOYMENT_HELPER_ID, $id); + } + + return $this->id = $id; + } +} diff --git a/src/Services/UpgradeManager.php b/src/Services/UpgradeManager.php index a3fb22c..03a058f 100644 --- a/src/Services/UpgradeManager.php +++ b/src/Services/UpgradeManager.php @@ -24,6 +24,7 @@ public function __construct( private readonly OneTimeTasks $oneTimeTasks, private readonly ProjectConfiguration $configuration, private readonly AccountService $accountService, + private readonly TrackingService $trackingService, ) { } @@ -47,8 +48,10 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->processHelper->console(['messenger:setup-transports']); - if ($this->state->getPreviousVersion() !== $this->state->getCurrentVersion()) { - $output->writeln(\sprintf('Updating Shopware from %s to %s', $this->state->getPreviousVersion(), $this->state->getCurrentVersion())); + $previousVersion = $this->state->getPreviousVersion(); + $currentVersion = $this->state->getCurrentVersion(); + if ($previousVersion !== $currentVersion) { + $output->writeln(\sprintf('Updating Shopware from %s to %s', $previousVersion, $currentVersion)); $additionalUpdateParameters = []; @@ -56,8 +59,16 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $additionalUpdateParameters[] = '--skip-asset-build'; } + $took = microtime(true); + $this->processHelper->console(['system:update:finish', ...$additionalUpdateParameters]); - $this->state->setVersion($this->state->getCurrentVersion()); + + $this->state->setVersion($currentVersion); + + $this->trackingService->track('upgrade', [ + 'took' => microtime(true) - $took, + 'previous_shopware_version' => $previousVersion, + ]); } $salesChannelUrl = EnvironmentHelper::getVariable('SALES_CHANNEL_URL'); @@ -81,7 +92,7 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->pluginHelper->removePlugins($configuration->skipAssetsInstall); if ($this->configuration->store->licenseDomain !== '') { - $this->accountService->refresh(new SymfonyStyle(new ArgvInput([]), $output), $this->state->getCurrentVersion(), $this->configuration->store->licenseDomain); + $this->accountService->refresh(new SymfonyStyle(new ArgvInput([]), $output), $currentVersion, $this->configuration->store->licenseDomain); } $this->appHelper->installApps(); @@ -90,7 +101,9 @@ public function run(RunConfiguration $configuration, OutputInterface $output): v $this->appHelper->removeApps(); if (!$configuration->skipThemeCompile) { + $took = microtime(true); $this->processHelper->console(['theme:compile', '--active-only']); + $this->trackingService->track('theme_compiled', ['took' => microtime(true) - $took]); } // Execute one-time tasks that should run after the update diff --git a/tests/Command/RunCommandTest.php b/tests/Command/RunCommandTest.php index f322173..e732185 100644 --- a/tests/Command/RunCommandTest.php +++ b/tests/Command/RunCommandTest.php @@ -10,6 +10,7 @@ use Shopware\Deployment\Services\HookExecutor; use Shopware\Deployment\Services\InstallationManager; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Services\UpgradeManager; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -26,6 +27,11 @@ public function testInstall(): void ->method('isInstalled') ->willReturn(false); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('5.7.0'); + $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor ->expects($this->exactly(2)) @@ -43,12 +49,27 @@ public function testInstall(): void return true; })); + $trackingService = $this->createMock(TrackingService::class); + $trackingService + ->expects($this->exactly(2)) + ->method('track') + ->willReturnCallback(function ($event, $data): void { + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('5.7.0', $data['mysql_version']); + } + }); + $command = new RunCommand( $state, $installationManager, $this->createMock(UpgradeManager::class), $hookExecutor, - new EventDispatcher() + new EventDispatcher(), + $trackingService ); $tester = new CommandTester($command); @@ -65,6 +86,10 @@ public function testUpdate(): void ->expects($this->once()) ->method('isInstalled') ->willReturn(true); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('5.7.0'); $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor @@ -88,12 +113,27 @@ public function testUpdate(): void return true; })); + $trackingService = $this->createMock(TrackingService::class); + $trackingService + ->expects($this->exactly(2)) + ->method('track') + ->willReturnCallback(function ($event, $data): void { + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('5.7.0', $data['mysql_version']); + } + }); + $command = new RunCommand( $state, $installationManager, $upgradeManager, $hookExecutor, - new EventDispatcher() + new EventDispatcher(), + $trackingService ); $tester = new CommandTester($command); @@ -115,6 +155,10 @@ public function testRunWithoutFullyInstalled(): void ->expects($this->once()) ->method('getPreviousVersion') ->willReturn('unknown'); + $state + ->expects($this->once()) + ->method('getMySqlVersion') + ->willReturn('10.6.0'); $hookExecutor = $this->createMock(HookExecutor::class); $hookExecutor @@ -131,12 +175,27 @@ public function testRunWithoutFullyInstalled(): void return true; })); + $trackingService = $this->createMock(TrackingService::class); + $trackingService + ->expects($this->exactly(2)) + ->method('track') + ->willReturnCallback(function ($event, $data): void { + if ($event === 'php_version') { + static::assertArrayHasKey('php_version', $data); + static::assertMatchesRegularExpression('/^\d+\.\d+$/', $data['php_version']); + } elseif ($event === 'mysql_version') { + static::assertArrayHasKey('mysql_version', $data); + static::assertEquals('10.6.0', $data['mysql_version']); + } + }); + $command = new RunCommand( $state, $installationManager, $this->createMock(UpgradeManager::class), $hookExecutor, - new EventDispatcher() + new EventDispatcher(), + $trackingService ); $tester = new CommandTester($command); diff --git a/tests/Helper/ProcessHelperTest.php b/tests/Helper/ProcessHelperTest.php index 0e923e0..96ccf01 100644 --- a/tests/Helper/ProcessHelperTest.php +++ b/tests/Helper/ProcessHelperTest.php @@ -7,16 +7,29 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Shopware\Deployment\Helper\ProcessHelper; +use Shopware\Deployment\Tests\TestUtil\BufferedConsoleOutput; #[CoversClass(ProcessHelper::class)] class ProcessHelperTest extends TestCase { + public function testOutputIsBuffered(): void + { + $output = new BufferedConsoleOutput(); + $helper = new ProcessHelper('/tmp', output: $output); + + $helper->runAndTail('echo "test"'); + + static::assertStringContainsString('Start: echo "test"', $output->getStdout()); + static::assertStringContainsString('test', $output->getStdout()); + static::assertStringContainsString('End: echo "test"', $output->getStdout()); + } + public function testRunAndTailReplacesPhpBinPlaceholder(): void { $tempFile = tempnam(sys_get_temp_dir(), 'php_bin_test_'); try { - $helper = new ProcessHelper('/tmp'); + $helper = new ProcessHelper('/tmp', output: new BufferedConsoleOutput()); $helper->runAndTail('echo %php.bin% > ' . $tempFile); static::assertFileExists($tempFile); diff --git a/tests/Services/InstallationManagerTest.php b/tests/Services/InstallationManagerTest.php index b78aefa..3d38ae8 100644 --- a/tests/Services/InstallationManagerTest.php +++ b/tests/Services/InstallationManagerTest.php @@ -15,6 +15,7 @@ use Shopware\Deployment\Services\InstallationManager; use Shopware\Deployment\Services\PluginHelper; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Output\OutputInterface; use Zalas\PHPUnit\Globals\Attribute\Env; @@ -39,6 +40,7 @@ public function testRun(): void $hookExecutor, new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -65,6 +67,7 @@ public function testRunNoStorefront(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -97,6 +100,7 @@ public function testRunDisabledAssetCopyAndThemeCompile(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(true, true), $this->createMock(OutputInterface::class)); @@ -127,6 +131,7 @@ public function testRunWithLicenseDomain(): void $hookExecutor, $configuration, $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -146,6 +151,9 @@ public function testRunWithForceReinstall(): void $accountService = $this->createMock(AccountService::class); $accountService->expects(static::never())->method('refresh'); + $trackingService = $this->createMock(TrackingService::class); + $trackingService->expects(static::once())->method('persistId'); + $manager = new InstallationManager( $this->createMock(ShopwareState::class), $this->createMock(Connection::class), @@ -155,6 +163,7 @@ public function testRunWithForceReinstall(): void $this->createMock(HookExecutor::class), new ProjectConfiguration(), $accountService, + $trackingService, ); $manager->run(new RunConfiguration(true, true, forceReinstallation: true), $this->createMock(OutputInterface::class)); diff --git a/tests/Services/ShopwareStateTest.php b/tests/Services/ShopwareStateTest.php index 5e21cfb..4f454c9 100644 --- a/tests/Services/ShopwareStateTest.php +++ b/tests/Services/ShopwareStateTest.php @@ -7,6 +7,7 @@ use Composer\InstalledVersions; use Doctrine\DBAL\Connection; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Shopware\Deployment\Services\ShopwareState; @@ -225,4 +226,67 @@ public function testDisableMaintenanceMode(): void $this->state->disableMaintenanceMode(); } + + #[DataProvider('mysqlVersionProvider')] + public function testGetMySqlVersion(string $versionString, string $expectedResult): void + { + $this->connection + ->expects($this->once()) + ->method('fetchOne') + ->with('SELECT VERSION()') + ->willReturn($versionString); + + static::assertSame($expectedResult, $this->state->getMySqlVersion()); + } + + /** + * @return array + */ + public static function mysqlVersionProvider(): array + { + return [ + 'MySQL 8.0' => [ + 'versionString' => '8.0.23', + 'expectedResult' => 'mysql-8.0.23', + ], + 'MySQL 5.7 with Ubuntu suffix' => [ + 'versionString' => '5.7.33-0ubuntu0.18.04.1', + 'expectedResult' => 'mysql-5.7.33', + ], + 'MariaDB 10.5' => [ + 'versionString' => '10.5.9-MariaDB', + 'expectedResult' => 'mariadb-10.5', + ], + 'MariaDB with complex version string' => [ + 'versionString' => '5.5.5-10.6.7-MariaDB-1:10.6.7+maria~focal', + 'expectedResult' => 'mariadb-10.6', + ], + 'MariaDB alternative format' => [ + 'versionString' => 'mariadb-10.11.2', + 'expectedResult' => 'mariadb-10.11', + ], + 'MariaDB uppercase' => [ + 'versionString' => '10.3.31-MARIADB-0ubuntu0.20.04.1', + 'expectedResult' => 'mariadb-10.3', + ], + 'Percona Server' => [ + 'versionString' => '8.0.25-15', + 'expectedResult' => 'mysql-8.0.25', + ], + ]; + } + + public function testGetMySqlVersionWithInvalidMariaDBVersion(): void + { + $this->connection + ->expects($this->once()) + ->method('fetchOne') + ->with('SELECT VERSION()') + ->willReturn('mariadb-invalid'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Invalid version string: mariadb-invalid'); + + $this->state->getMySqlVersion(); + } } diff --git a/tests/Services/TrackingServiceTest.php b/tests/Services/TrackingServiceTest.php new file mode 100644 index 0000000..ed8516b --- /dev/null +++ b/tests/Services/TrackingServiceTest.php @@ -0,0 +1,66 @@ +createMock(ShopwareState::class); + $shopwareState->method('getCurrentVersion')->willReturn('6.6.0.0'); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->track('test_event'); + + $id = $systemConfigHelper->get('core.deployment_helper.id'); + static::assertNotNull($id); + static::assertMatchesRegularExpression('/^[a-f0-9]{32}$/', $id); + } + + public function testTrackUsesExistingIdFromConfig(): void + { + $systemConfigHelper = new StaticSystemConfigHelper(); + $systemConfigHelper->set('core.deployment_helper.id', 'existing-id-123'); + $shopwareState = $this->createMock(ShopwareState::class); + $shopwareState->method('getCurrentVersion')->willReturn('6.6.0.0'); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->track('test_event'); + + static::assertSame('existing-id-123', $systemConfigHelper->get('core.deployment_helper.id')); + } + + public function testPersistIdDoesNothingWhenNoIdGenerated(): void + { + $systemConfigHelper = new StaticSystemConfigHelper(); + $shopwareState = $this->createMock(ShopwareState::class); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->persistId(); + + static::assertNull($systemConfigHelper->get('core.deployment_helper.id')); + } + + #[Env('DO_NOT_TRACK')] + #[DoesNotPerformAssertions] + public function testTrackWithDoNotTrackEnv(): void + { + $systemConfigHelper = new StaticSystemConfigHelper(); + $shopwareState = $this->createMock(ShopwareState::class); + + $service = new TrackingService($systemConfigHelper, $shopwareState); + $service->track('test_event'); + } +} diff --git a/tests/Services/UpgradeManagerTest.php b/tests/Services/UpgradeManagerTest.php index 244a3fa..b77a5a3 100644 --- a/tests/Services/UpgradeManagerTest.php +++ b/tests/Services/UpgradeManagerTest.php @@ -14,6 +14,7 @@ use Shopware\Deployment\Services\OneTimeTasks; use Shopware\Deployment\Services\PluginHelper; use Shopware\Deployment\Services\ShopwareState; +use Shopware\Deployment\Services\TrackingService; use Shopware\Deployment\Services\UpgradeManager; use Shopware\Deployment\Struct\RunConfiguration; use Symfony\Component\Console\Output\OutputInterface; @@ -47,6 +48,7 @@ public function testRun(): void $oneTimeTasks, new ProjectConfiguration(), $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -56,12 +58,12 @@ public function testRunUpdatesVersion(): void { $state = $this->createMock(ShopwareState::class); $state - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getCurrentVersion') ->willReturn('1.0.0'); $state - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPreviousVersion') ->willReturn('0.0.0'); @@ -79,6 +81,7 @@ public function testRunUpdatesVersion(): void $this->createMock(OneTimeTasks::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -88,12 +91,12 @@ public function testRunUpdatesVersionNoAssetCompile(): void { $state = $this->createMock(ShopwareState::class); $state - ->expects($this->exactly(3)) + ->expects($this->once()) ->method('getCurrentVersion') ->willReturn('1.0.0'); $state - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('getPreviousVersion') ->willReturn('0.0.0'); @@ -120,6 +123,7 @@ public function testRunUpdatesVersionNoAssetCompile(): void $this->createMock(OneTimeTasks::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(true, true), $this->createMock(OutputInterface::class)); @@ -163,6 +167,7 @@ public function testRunWithDifferentSalesChannelUrl(): void $this->createMock(OneTimeTasks::class), new ProjectConfiguration(), $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -205,6 +210,7 @@ public function testRunWithMaintenanceMode(): void $this->createMock(OneTimeTasks::class), $config, $this->createMock(AccountService::class), + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); @@ -242,6 +248,7 @@ public function testRunWithLicenseDomain(): void $oneTimeTasks, $configuration, $accountService, + $this->createMock(TrackingService::class), ); $manager->run(new RunConfiguration(), $this->createMock(OutputInterface::class)); diff --git a/tests/TestUtil/BufferedConsoleOutput.php b/tests/TestUtil/BufferedConsoleOutput.php new file mode 100644 index 0000000..afb9490 --- /dev/null +++ b/tests/TestUtil/BufferedConsoleOutput.php @@ -0,0 +1,33 @@ +stdout .= $message; + } + + public function writeStderr(string $message): void + { + $this->stderr .= $message; + } + + public function getStdout(): string + { + return $this->stdout; + } + + public function getStderr(): string + { + return $this->stderr; + } +}