* Dariusz Rumiński * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Tests\AutoReview; use PhpCsFixer\Preg; use PhpCsFixer\Tests\TestCase; use PhpCsFixer\Tokenizer\Tokens; use PHPUnit\Framework\Constraint\TraversableContainsIdentical; use Symfony\Component\Yaml\Yaml; /** * @author Dariusz Rumiński * * @internal * * @coversNothing * * @group auto-review * @group covers-nothing */ final class CiConfigurationTest extends TestCase { public function testThatPhpVersionEnvsAreSetProperly(): void { self::assertSame( [ 'PHP_MAX' => $this->getMaxPhpVersionFromEntryFile(), 'PHP_MIN' => $this->getMinPhpVersionFromEntryFile(), ], $this->getGitHubCiEnvs(), ); } public function testTestJobsRunOnEachPhp(): void { $supportedVersions = []; $supportedMinPhp = (float) $this->getMinPhpVersionFromEntryFile(); $supportedMaxPhp = (float) $this->getMaxPhpVersionFromEntryFile(); if ($supportedMaxPhp >= 8) { $supportedVersions = array_merge( $supportedVersions, self::generateMinorVersionsRange($supportedMinPhp, 7.4) ); $supportedMinPhp = 8; } $supportedVersions = [ ...$supportedVersions, ...self::generateMinorVersionsRange($supportedMinPhp, $supportedMaxPhp), ]; self::assertTrue(\count($supportedVersions) > 0); $ciVersions = $this->getAllPhpVersionsUsedByCiForTests(); self::assertNotEmpty($ciVersions); self::assertSupportedPhpVersionsAreCoveredByCiJobs($supportedVersions, $ciVersions); self::assertUpcomingPhpVersionIsCoveredByCiJob(end($supportedVersions), $ciVersions); self::assertSupportedPhpVersionsAreCoveredByCiJobs($supportedVersions, $this->getPhpVersionsUsedForBuildingOfficialImages()); self::assertSupportedPhpVersionsAreCoveredByCiJobs($supportedVersions, $this->getPhpVersionsUsedForBuildingLocalImages()); } public function testDeploymentJobRunOnLatestStablePhpThatIsSupportedByTool(): void { $ciVersionsForDeployment = $this->getPhpVersionUsedByCiForDeployments(); $ciVersions = $this->getAllPhpVersionsUsedByCiForTests(); $expectedPhp = $this->getMaxPhpVersionFromEntryFile(); if (\in_array($expectedPhp.'snapshot', $ciVersions, true)) { // last version of used PHP is snapshot. we should test against previous one, that is stable $expectedPhp = (string) ((float) $expectedPhp - 0.1); } self::assertTrue( version_compare($expectedPhp, $ciVersionsForDeployment, 'eq'), \sprintf('Expects %s to be %s', $ciVersionsForDeployment, $expectedPhp) ); } /** * @return list */ private static function generateMinorVersionsRange(float $from, float $to): array { $range = []; for ($version = $from; $version <= $to; $version += 0.1) { $range[] = \sprintf('%.1f', $version); } return $range; } private static function ensureTraversableContainsIdenticalIsAvailable(): void { if (!class_exists(TraversableContainsIdentical::class)) { self::markTestSkipped('TraversableContainsIdentical not available.'); } } /** * @param numeric-string $lastSupportedVersion * @param list $ciVersions */ private static function assertUpcomingPhpVersionIsCoveredByCiJob(string $lastSupportedVersion, array $ciVersions): void { self::ensureTraversableContainsIdenticalIsAvailable(); self::assertThat($ciVersions, self::logicalOr( // if `$lastsupportedVersion` is already a snapshot version new TraversableContainsIdentical(\sprintf('%.1fsnapshot', $lastSupportedVersion)), // if `$lastsupportedVersion` is not snapshot version, expect CI to run snapshot of next PHP version new TraversableContainsIdentical('nightly'), new TraversableContainsIdentical(\sprintf('%.1fsnapshot', $lastSupportedVersion + 0.1)), // GitHub CI uses just versions, without suffix, e.g. 8.1 for 8.1snapshot as of writing new TraversableContainsIdentical(\sprintf('%.1f', $lastSupportedVersion + 0.1)), new TraversableContainsIdentical(\sprintf('%.1f', floor($lastSupportedVersion + 1.0))) )); } /** * @param list $supportedVersions * @param list $ciVersions */ private static function assertSupportedPhpVersionsAreCoveredByCiJobs(array $supportedVersions, array $ciVersions): void { $lastSupportedVersion = array_pop($supportedVersions); foreach ($supportedVersions as $expectedVersion) { self::assertContains($expectedVersion, $ciVersions); } self::ensureTraversableContainsIdenticalIsAvailable(); self::assertThat($ciVersions, self::logicalOr( new TraversableContainsIdentical($lastSupportedVersion), new TraversableContainsIdentical(\sprintf('%.1fsnapshot', $lastSupportedVersion)) )); } private function getPhpVersionUsedByCiForDeployments(): string { $yaml = Yaml::parse(file_get_contents(__DIR__.'/../../.github/workflows/ci.yml')); $version = $yaml['jobs']['deployment']['env']['php-version']; return \is_string($version) ? $version : \sprintf('%.1f', $version); } /** * @return list */ private function getAllPhpVersionsUsedByCiForTests(): array { return $this->getPhpVersionsUsedByGitHub(); } private function convertPhpVerIdToNiceVer(string $verId): string { $matchResult = Preg::match('/^(?\d{1,2})_?(?\d{2})_?(?\d{2})$/', $verId, $capture); if (!$matchResult) { throw new \LogicException(\sprintf('Can\'t parse version "%s" id.', $verId)); } return \sprintf('%d.%d', $capture['major'], $capture['minor']); } private function getMaxPhpVersionFromEntryFile(): string { $tokens = Tokens::fromCode(file_get_contents(__DIR__.'/../../php-cs-fixer')); $sequence = $tokens->findSequence([ [T_STRING, 'PHP_VERSION_ID'], [T_IS_GREATER_OR_EQUAL], [T_INT_CAST], [T_CONSTANT_ENCAPSED_STRING], ]); if (null === $sequence) { throw new \LogicException("Can't find version - perhaps entry file was modified?"); } $phpVerId = trim(end($sequence)->getContent(), '\''); return $this->convertPhpVerIdToNiceVer((string) ((int) $phpVerId - 100)); } private function getMinPhpVersionFromEntryFile(): string { $tokens = Tokens::fromCode(file_get_contents(__DIR__.'/../../php-cs-fixer')); $sequence = $tokens->findSequence([ [T_STRING, 'PHP_VERSION_ID'], '<', [T_INT_CAST], [T_CONSTANT_ENCAPSED_STRING], ]); if (null === $sequence) { throw new \LogicException("Can't find version - perhaps entry file was modified?"); } $phpVerId = trim(end($sequence)->getContent(), '\''); return $this->convertPhpVerIdToNiceVer($phpVerId); } /** * @return array */ private function getGitHubCiEnvs(): array { $yaml = Yaml::parse(file_get_contents(__DIR__.'/../../.github/workflows/ci.yml')); return $yaml['env']; } /** * @return list */ private function getPhpVersionsUsedByGitHub(): array { $yaml = Yaml::parse(file_get_contents(__DIR__.'/../../.github/workflows/ci.yml')); $phpVersions = $yaml['jobs']['tests']['strategy']['matrix']['php-version'] ?? []; foreach ($yaml['jobs']['tests']['strategy']['matrix']['include'] as $job) { $phpVersions[] = $job['php-version']; } return $phpVersions; } /** * @return list */ private function getPhpVersionsUsedForBuildingOfficialImages(): array { $yaml = Yaml::parse(file_get_contents(__DIR__.'/../../.github/workflows/release.yml')); return array_map( static fn ($item) => $item['php-version'], $yaml['jobs']['docker-images']['strategy']['matrix']['include'] ); } /** * @return list */ private function getPhpVersionsUsedForBuildingLocalImages(): array { $yaml = Yaml::parse(file_get_contents(__DIR__.'/../../.github/workflows/docker.yml')); return array_map( static fn ($item) => $item['php-version'], $yaml['jobs']['docker-compose-build']['strategy']['matrix']['include'] ); } }