new FeatureStatus(DiviFeature::PageRead, ReleaseState::Released, [], 'code'), // Under the wrong key: does not count for page-write. 'page-write' => new FeatureStatus(DiviFeature::PageRead, ReleaseState::Released, [], 'code'), ]); self::assertTrue($matrix->of(DiviFeature::PageRead)->usable()); foreach ([DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite] as $feature) { $status = $matrix->of($feature); self::assertSame(ReleaseState::Incompatible, $status->state); self::assertSame([CompatibilityMatrix::MISSING], $status->problems); } self::assertCount(count(DiviFeature::cases()) - 1, $matrix->unusable()); } public function testAllReleasedAndAllIncompatible(): void { $released = CompatibilityMatrix::allReleased(DiviFeature::cases(), 'database'); $broken = CompatibilityMatrix::allIncompatible(DiviFeature::cases(), 'kaputt'); foreach (DiviFeature::cases() as $feature) { self::assertTrue($released->of($feature)->usable()); self::assertSame('database', $released->of($feature)->source); self::assertSame(ReleaseState::Incompatible, $broken->of($feature)->state); self::assertSame(['kaputt'], $broken->of($feature)->problems); self::assertNull($broken->of($feature)->source); } self::assertSame([], $released->unusable()); self::assertSame('code', CompatibilityMatrix::allReleased(DiviFeature::cases())->source()); } public function testTheStateIsTheWorstFeatureAndEveryProblemIsListedOnce(): void { $matrix = self::matrix([ 'page-write' => [ReleaseState::Untested, ['a']], 'tb-write' => [ReleaseState::Incompatible, ['b', 'a']], ]); self::assertSame(ReleaseState::Incompatible, $matrix->state()); self::assertSame(['a', 'b'], $matrix->problems()); self::assertNull($matrix->source()); self::assertSame(ReleaseState::Untested, self::matrix(['catalog' => [ReleaseState::Untested, []]])->state()); self::assertSame(ReleaseState::Released, self::matrix([])->state()); } public function testTheSourceOfAReleasedMatrixIsCommonOrNull(): void { self::assertSame('auto', self::matrix([])->source()); $mixed = new CompatibilityMatrix(DiviFeature::cases(), array_merge( self::features([]), ['roles' => new FeatureStatus(DiviFeature::Roles, ReleaseState::Released, [], 'code')] )); self::assertSame(ReleaseState::Released, $mixed->state()); self::assertNull($mixed->source()); } public function testUsableAndUnusable(): void { $matrix = self::matrix(['catalog' => [ReleaseState::Untested, []]]); self::assertTrue($matrix->usable([DiviFeature::PageWrite, DiviFeature::PageRead])); self::assertFalse($matrix->usable([DiviFeature::PageWrite, DiviFeature::Catalog])); self::assertTrue($matrix->usable([])); self::assertSame( [DiviFeature::Catalog], array_map(static fn (FeatureStatus $status): DiviFeature => $status->feature, $matrix->unusable()) ); } public function testLockedNamesEveryUnusableFeatureOnceInTheGivenOrder(): void { $matrix = ReleaseMatrices::matrix( [DiviFeature::LibraryWrite, DiviFeature::ThemeBuilderWrite, DiviFeature::Catalog], ReleaseState::Incompatible ); $locked = $matrix->locked([ DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite, DiviFeature::LibraryWrite, DiviFeature::ThemeBuilderWrite, ]); self::assertSame( [DiviFeature::ThemeBuilderWrite, DiviFeature::LibraryWrite], array_map(static fn (FeatureStatus $status): DiviFeature => $status->feature, $locked) ); self::assertSame(ReleaseState::Incompatible, $locked[0]->state); self::assertSame([], $matrix->locked([DiviFeature::PageWrite, DiviFeature::PageRead])); self::assertSame([], $matrix->locked([])); } public function testToArrayListsEveryFeatureInCaseOrder(): void { $rows = self::matrix(['tb-write' => [ReleaseState::Incompatible, ['b']]])->toArray(); self::assertSame( array_map(static fn (DiviFeature $feature): string => $feature->value, DiviFeature::cases()), array_column($rows, 'id') ); self::assertContains( ['id' => 'tb-write', 'label' => DiviFeature::ThemeBuilderWrite->label(), 'state' => 'incompatible', 'problems' => ['b'], 'source' => null], $rows ); } /** * @param array}> $locked * @return array */ private static function features(array $locked): array { $features = []; foreach (DiviFeature::cases() as $feature) { [$state, $problems] = $locked[$feature->value] ?? [ReleaseState::Released, []]; $features[$feature->value] = new FeatureStatus( $feature, $state, $problems, $state === ReleaseState::Released ? 'auto' : null ); } return $features; } /** @param array}> $locked */ private static function matrix(array $locked): CompatibilityMatrix { return new CompatibilityMatrix(DiviFeature::cases(), self::features($locked)); } private static function fakeCapability(string $id, bool $write = false): Capability { return new class ($id, $write) implements Capability { public function __construct(private readonly string $id, private readonly bool $write) { } public function id(): string { return $this->id; } public function label(): string { return 'Fake ' . $this->id; } public function isWrite(): bool { return $this->write; } }; } public function testWorksForCapabilitiesOfAnotherIntegration(): void { $read = self::fakeCapability('pods-x'); $write = self::fakeCapability('pods-y', true); $stranger = self::fakeCapability('pods-z'); $matrix = CompatibilityMatrix::uniform([$read, $write], ReleaseState::Untested, ['offen']); self::assertSame(ReleaseState::Untested, $matrix->of($read)->state); self::assertSame(['pods-x', 'pods-y'], array_column($matrix->toArray(), 'id')); self::assertFalse($matrix->usable([$read, $write])); self::assertSame( [CompatibilityMatrix::MISSING], $matrix->of($stranger)->problems, 'a capability the matrix does not know is incompatible (fail closed)' ); self::assertSame(ReleaseState::Released, CompatibilityMatrix::allReleased([$read])->state()); self::assertSame( ReleaseState::Incompatible, CompatibilityMatrix::allIncompatible([$read], 'kaputt')->of($read)->state ); } public function testTheSourceConstantsLiveOnTheMatrix(): void { self::assertSame('code', CompatibilityMatrix::SOURCE_CODE); self::assertSame('database', CompatibilityMatrix::SOURCE_DATABASE); self::assertSame('auto', CompatibilityMatrix::SOURCE_AUTO); } public function testFromProblemsWeighsHardAndSoftProblemsPerCapability(): void { $a = self::fakeCapability('pods-a'); $b = self::fakeCapability('pods-b'); $c = self::fakeCapability('pods-c'); $problems = [ new Problem('hart', [$a], Problem::CALL), new Problem('Signatur', [$b], Problem::SIGNATURE), new Problem('Referenz', [$c], Problem::REFERENCE_MISSING), ]; $released = CompatibilityMatrix::fromProblems([$a, $b, $c], $problems, CompatibilityMatrix::SOURCE_CODE, false); self::assertSame(ReleaseState::Incompatible, $released->of($a)->state, 'hard locks even a released version'); self::assertSame(['hart'], $released->of($a)->problems); self::assertSame(CompatibilityMatrix::SOURCE_CODE, $released->of($b)->source); $manual = CompatibilityMatrix::fromProblems([$a, $b, $c], $problems, null, false); self::assertSame(ReleaseState::Untested, $manual->of($b)->state); self::assertSame(['Signatur'], $manual->of($b)->problems); $auto = CompatibilityMatrix::fromProblems([$a, $b, $c], $problems, null, true); self::assertSame(ReleaseState::Incompatible, $auto->of($b)->state, 'changed signature'); self::assertSame(ReleaseState::Untested, $auto->of($c)->state, 'reference missing'); $clean = CompatibilityMatrix::fromProblems([$a], [new Problem('b', [$b], Problem::CALL)], null, true); self::assertSame(ReleaseState::Released, $clean->of($a)->state); self::assertSame(CompatibilityMatrix::SOURCE_AUTO, $clean->of($a)->source); $everyone = CompatibilityMatrix::fromProblems([$a, $b], [new Problem('alle', [], Problem::CALL)], null, true); self::assertSame([ReleaseState::Incompatible, ReleaseState::Incompatible], [ $everyone->of($a)->state, $everyone->of($b)->state, ], 'a problem without capabilities concerns every one'); } }