diff --git a/src/Command/SyncScriptMappingsCommand.php b/src/Command/SyncScriptMappingsCommand.php new file mode 100644 index 0000000..cfcee0e --- /dev/null +++ b/src/Command/SyncScriptMappingsCommand.php @@ -0,0 +1,55 @@ +scriptMappingRepository->findAllOrderedByPublicPath() as $mapping) { + try { + $this->gitSynchronizer->sync($mapping); + ++$synchronized; + } catch (GitSyncFailedException) { + ++$failed; + $io->error(sprintf('Mapping "%s" synchronization failed.', $mapping->getPublicPath())); + } finally { + $this->entityManager->flush(); + } + } + + $summary = sprintf('%d mapping(s) synchronized, %d failed.', $synchronized, $failed); + if ($failed === 0) { + $io->success($summary); + + return Command::SUCCESS; + } + + $io->warning($summary); + + return Command::FAILURE; + } +} diff --git a/tests/Command/SyncScriptMappingsCommandTest.php b/tests/Command/SyncScriptMappingsCommandTest.php new file mode 100644 index 0000000..97ed373 --- /dev/null +++ b/tests/Command/SyncScriptMappingsCommandTest.php @@ -0,0 +1,139 @@ +filesystem = new Filesystem(); + $this->workDir = sys_get_temp_dir().'/get-installer-bootstrap-command-test-'.bin2hex(random_bytes(6)); + $this->filesystem->mkdir($this->workDir); + } + + protected function tearDown(): void + { + $this->filesystem->remove($this->workDir); + + parent::tearDown(); + } + + public function testCommandSynchronizesEveryMapping(): void + { + $firstRepositoryDir = $this->createRepository('first', [ + 'install.sh' => "#!/usr/bin/env bash\nprintf 'first'\n", + ]); + $secondRepositoryDir = $this->createRepository('second', [ + 'install.sh' => "#!/usr/bin/env bash\nprintf 'second'\n", + ]); + + $firstMapping = $this->mapping('mcp/first/install.sh', $firstRepositoryDir); + $secondMapping = $this->mapping('mcp/second/install.sh', $secondRepositoryDir); + $this->entityManager->persist($firstMapping); + $this->entityManager->persist($secondMapping); + $this->entityManager->flush(); + + $application = new Application(static::$kernel); + self::assertTrue($application->has('app:mappings:sync')); + + $commandTester = new CommandTester($application->find('app:mappings:sync')); + $commandTester->execute([]); + + self::assertSame(0, $commandTester->getStatusCode()); + self::assertStringContainsString('2 mapping(s) synchronized, 0 failed.', $commandTester->getDisplay()); + + $this->entityManager->clear(); + $syncedFirstMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($firstMapping->getId()); + $syncedSecondMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($secondMapping->getId()); + + self::assertInstanceOf(ScriptMapping::class, $syncedFirstMapping); + self::assertInstanceOf(ScriptMapping::class, $syncedSecondMapping); + self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $syncedFirstMapping->getLastSyncStatus()); + self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $syncedSecondMapping->getLastSyncStatus()); + } + + public function testCommandContinuesAfterMappingFailureAndReturnsFailure(): void + { + $failingRepositoryDir = $this->createRepository('failing', [ + 'install.sh' => "#!/usr/bin/env bash\nprintf 'missing target'\n", + ]); + $workingRepositoryDir = $this->createRepository('working', [ + 'install.sh' => "#!/usr/bin/env bash\nprintf 'working'\n", + ]); + + $failingMapping = $this->mapping('mcp/a-failing/install.sh', $failingRepositoryDir) + ->setRepositoryFilePath('missing.sh'); + $workingMapping = $this->mapping('mcp/b-working/install.sh', $workingRepositoryDir); + $this->entityManager->persist($failingMapping); + $this->entityManager->persist($workingMapping); + $this->entityManager->flush(); + + $application = new Application(static::$kernel); + self::assertTrue($application->has('app:mappings:sync')); + + $commandTester = new CommandTester($application->find('app:mappings:sync')); + $commandTester->execute([]); + + self::assertSame(1, $commandTester->getStatusCode()); + self::assertStringContainsString('1 mapping(s) synchronized, 1 failed.', $commandTester->getDisplay()); + + $this->entityManager->clear(); + $syncedFailingMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($failingMapping->getId()); + $syncedWorkingMapping = $this->entityManager->getRepository(ScriptMapping::class)->find($workingMapping->getId()); + + self::assertInstanceOf(ScriptMapping::class, $syncedFailingMapping); + self::assertInstanceOf(ScriptMapping::class, $syncedWorkingMapping); + self::assertSame(ScriptMapping::SYNC_STATUS_FAILED, $syncedFailingMapping->getLastSyncStatus()); + self::assertSame(ScriptMapping::SYNC_STATUS_SYNCED, $syncedWorkingMapping->getLastSyncStatus()); + } + + private function mapping(string $publicPath, string $repositoryDir): ScriptMapping + { + return (new ScriptMapping()) + ->setPublicPath($publicPath) + ->setRepositoryUrl($repositoryDir) + ->setGitRef('main') + ->setRepositoryFilePath('install.sh') + ->setActive(true); + } + + /** @param array $files */ + private function createRepository(string $name, array $files): string + { + $repositoryDir = $this->workDir.'/'.$name; + $this->filesystem->mkdir($repositoryDir); + + foreach ($files as $path => $content) { + $fullPath = $repositoryDir.'/'.$path; + $this->filesystem->mkdir(dirname($fullPath)); + file_put_contents($fullPath, $content); + } + + $this->runGit(['init'], $repositoryDir); + $this->runGit(['config', 'user.email', 'tests@example.com'], $repositoryDir); + $this->runGit(['config', 'user.name', 'Tests'], $repositoryDir); + $this->runGit(['add', '.'], $repositoryDir); + $this->runGit(['commit', '-m', 'Initial commit'], $repositoryDir); + $this->runGit(['branch', '-M', 'main'], $repositoryDir); + + return $repositoryDir; + } + + /** @param list $arguments */ + private function runGit(array $arguments, string $cwd): void + { + (new Process(['git', ...$arguments], $cwd))->mustRun(); + } +}