Requirement, Call, ConstantRequirement, Mirror, Problem, FeatureStatus, CompatibilityMatrix, Manifest, ManifestCheck sowie Fingerprints und LineDiff liegen jetzt im divi-freien Kern. Neu: GlobalRequirement, CompatibilityMatrix::fromProblems(), ManifestCheck-Option signatures, DiviManifestCheck::create() und DiviAreas. Divi-Verhalten unverändert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
243 lines
9.9 KiB
PHP
243 lines
9.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace LIT\MCP\Tests\Unit\Compatibility;
|
|
|
|
use LIT\MCP\Compatibility\Capability;
|
|
use LIT\MCP\Compatibility\CompatibilityMatrix;
|
|
use LIT\MCP\Compatibility\FeatureStatus;
|
|
use LIT\MCP\Compatibility\Problem;
|
|
use LIT\MCP\Compatibility\ReleaseState;
|
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
|
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class CompatibilityMatrixTest extends TestCase
|
|
{
|
|
public function testAFeatureWithoutResultIsIncompatible(): void
|
|
{
|
|
$matrix = new CompatibilityMatrix(DiviFeature::cases(), [
|
|
'page-read' => 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<string, array{ReleaseState, list<string>}> $locked
|
|
* @return array<string, FeatureStatus>
|
|
*/
|
|
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<string, array{ReleaseState, list<string>}> $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');
|
|
}
|
|
}
|