feat: Divi-Freigabe je Fähigkeit – CompatibilityMatrix aus Problemen mit Art und Fähigkeiten (ManifestCheck::problems), automatische Freigabe unveränderter Fähigkeiten (Filter lit_mcp_divi_auto_release), Mirror-Probleme im Transient (Format 2), ReleaseStatus trägt die Matrix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
710444e88d
commit
c4efeb3ba4
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class CompatibilityMatrixTest extends TestCase
|
||||
{
|
||||
public function testAFeatureWithoutResultIsIncompatible(): void
|
||||
{
|
||||
$matrix = new CompatibilityMatrix([
|
||||
'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('database');
|
||||
$broken = CompatibilityMatrix::allIncompatible('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()->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(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 testTheWriteFeaturesAreTheWritingCases(): void
|
||||
{
|
||||
self::assertSame(
|
||||
array_values(array_filter(DiviFeature::cases(), static fn (DiviFeature $f): bool => $f->isWrite())),
|
||||
CompatibilityMatrix::writeFeatures()
|
||||
);
|
||||
self::assertContains(DiviFeature::PageWrite, CompatibilityMatrix::writeFeatures());
|
||||
self::assertNotContains(DiviFeature::PageRead, CompatibilityMatrix::writeFeatures());
|
||||
}
|
||||
|
||||
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(self::features($locked));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\FeatureStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FeatureStatusTest extends TestCase
|
||||
{
|
||||
public function testOnlyAReleasedFeatureIsUsable(): void
|
||||
{
|
||||
self::assertTrue((new FeatureStatus(DiviFeature::PageRead, ReleaseState::Released, [], 'auto'))->usable());
|
||||
self::assertFalse((new FeatureStatus(DiviFeature::PageRead, ReleaseState::Untested))->usable());
|
||||
self::assertFalse((new FeatureStatus(DiviFeature::PageRead, ReleaseState::Incompatible, ['x']))->usable());
|
||||
}
|
||||
|
||||
public function testSummaryForRefusalsAndToArrayWithTheSource(): void
|
||||
{
|
||||
$status = new FeatureStatus(DiviFeature::PresetWrite, ReleaseState::Incompatible, ['Designsystem: x()']);
|
||||
|
||||
self::assertSame(
|
||||
['id' => 'preset-write', 'label' => DiviFeature::PresetWrite->label(), 'state' => 'incompatible',
|
||||
'problems' => ['Designsystem: x()']],
|
||||
$status->summary()
|
||||
);
|
||||
self::assertSame($status->summary() + ['source' => null], $status->toArray());
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,13 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
use ArrayObject;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Release\Call;
|
||||
use LIT\MCP\Integrations\Divi\Release\ConstantRequirement;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Manifest;
|
||||
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\Mirror;
|
||||
use LIT\MCP\Integrations\Divi\Release\Problem;
|
||||
use LIT\MCP\Integrations\Divi\Release\SignatureFingerprint;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -131,4 +135,101 @@ final class ManifestCheckTest extends TestCase
|
||||
ManifestCheck::failed($checks)
|
||||
);
|
||||
}
|
||||
|
||||
/** A manifest with one requirement per problem kind; the default feature is page-read. */
|
||||
private function featureCheck(?\Closure $current = null): ManifestCheck
|
||||
{
|
||||
$manifest = new Manifest(
|
||||
Manifest::PAGES,
|
||||
calls: [
|
||||
new Call('strlen', 1),
|
||||
new Call('str_replace', 3, [DiviFeature::PageWrite]),
|
||||
new Call('trim', 1),
|
||||
new Call('lit_mcp_missing_function', 0, [DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite]),
|
||||
],
|
||||
constants: [
|
||||
new ConstantRequirement('PHP_VERSION'),
|
||||
new ConstantRequirement('LIT_MCP_MISSING_CONSTANT', [DiviFeature::Catalog]),
|
||||
],
|
||||
postTypes: ['lit_mcp_missing_type'],
|
||||
mirrors: [
|
||||
new Mirror('same', 'a.php', 'a', null, 'gleich'),
|
||||
new Mirror('changed', 'b.php', 'b', null, 'geändert', [DiviFeature::ThemeBuilderWrite]),
|
||||
new Mirror('no-expectation', 'd.php', 'd', null, 'ohne Soll'),
|
||||
],
|
||||
features: [DiviFeature::PageRead]
|
||||
);
|
||||
$hashes = ['same' => 'h1', 'changed' => 'hX', 'no-expectation' => 'h4'];
|
||||
|
||||
return new ManifestCheck(
|
||||
['A' => $manifest],
|
||||
[
|
||||
'same' => 'h1',
|
||||
'changed' => 'h2',
|
||||
'sig:strlen' => (string) SignatureFingerprint::of(new Call('strlen', 1)),
|
||||
'sig:str_replace' => 'a different signature',
|
||||
],
|
||||
$current ?? static fn (Mirror $mirror): ?string => $hashes[$mirror->id] ?? null,
|
||||
static fn (string $name): bool => false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Problem> $problems
|
||||
* @return list<array{label: string, kind: string, features: list<string>}>
|
||||
*/
|
||||
private static function rows(array $problems): array
|
||||
{
|
||||
return array_map(static fn (Problem $problem): array => $problem->toArray(), $problems);
|
||||
}
|
||||
|
||||
public function testCodeProblemsNameKindAndFeaturesOfEveryFailedCall(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
['label' => 'Seiten: str_replace() – Signatur geändert', 'kind' => 'signature',
|
||||
'features' => ['page-write']],
|
||||
['label' => 'Seiten: trim() – Referenz der Signatur fehlt', 'kind' => 'reference-missing',
|
||||
'features' => ['page-read']],
|
||||
['label' => 'Seiten: lit_mcp_missing_function()', 'kind' => 'call',
|
||||
'features' => ['page-write', 'tb-write']],
|
||||
['label' => 'Seiten: Konstante LIT_MCP_MISSING_CONSTANT', 'kind' => 'constant',
|
||||
'features' => ['catalog']],
|
||||
],
|
||||
self::rows($this->featureCheck(static function (): never {
|
||||
self::fail('code problems never read Divi files');
|
||||
})->codeProblems())
|
||||
);
|
||||
}
|
||||
|
||||
public function testAMirrorWithoutReferenceIsAMirrorProblemLikeAChangedOne(): void
|
||||
{
|
||||
self::assertSame(
|
||||
[
|
||||
['label' => 'Seiten: Nachbau von b() – geändert', 'kind' => 'mirror', 'features' => ['tb-write']],
|
||||
['label' => 'Seiten: Nachbau von d() – ohne Soll', 'kind' => 'mirror', 'features' => ['page-read']],
|
||||
],
|
||||
self::rows($this->featureCheck()->mirrorProblems())
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnUnreadableMirrorIsAMirrorProblem(): void
|
||||
{
|
||||
$problems = $this->featureCheck(static function (): never {
|
||||
throw new RuntimeException('Divi file gone');
|
||||
})->mirrorProblems();
|
||||
|
||||
self::assertSame(['mirror', 'mirror', 'mirror'], array_column(self::rows($problems), 'kind'));
|
||||
}
|
||||
|
||||
public function testProblemsAreCodeThenMirrorsAndRegistrationIsNone(): void
|
||||
{
|
||||
$check = $this->featureCheck();
|
||||
|
||||
self::assertEquals(array_merge($check->codeProblems(), $check->mirrorProblems()), $check->problems());
|
||||
self::assertNotContains(
|
||||
'Seiten: Post-Typ lit_mcp_missing_type',
|
||||
array_column(self::rows($check->problems()), 'label')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\Problem;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ProblemTest extends TestCase
|
||||
{
|
||||
public function testOnlyCallMirrorConstantAndResolverAreHard(): void
|
||||
{
|
||||
$hard = [];
|
||||
foreach (['call', 'signature', 'mirror', 'constant', 'reference-missing', 'resolver'] as $kind) {
|
||||
$hard[$kind] = (new Problem('x', [DiviFeature::PageWrite], $kind))->isHard();
|
||||
}
|
||||
|
||||
self::assertSame(
|
||||
['call' => true, 'signature' => false, 'mirror' => true, 'constant' => true,
|
||||
'reference-missing' => false, 'resolver' => true],
|
||||
$hard
|
||||
);
|
||||
}
|
||||
|
||||
public function testConcernsItsFeaturesAndWithoutFeaturesEveryFeature(): void
|
||||
{
|
||||
$problem = new Problem('x', [DiviFeature::PageWrite, DiviFeature::ThemeBuilderWrite], Problem::CALL);
|
||||
self::assertTrue($problem->concerns(DiviFeature::ThemeBuilderWrite));
|
||||
self::assertFalse($problem->concerns(DiviFeature::PageRead));
|
||||
|
||||
$unassigned = new Problem('x', [], Problem::CALL);
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
self::assertTrue($unassigned->concerns($feature), $feature->value);
|
||||
}
|
||||
}
|
||||
|
||||
public function testRoundTripsThroughTheCacheFormat(): void
|
||||
{
|
||||
$problem = new Problem('Seiten: Nachbau von a() – x', [DiviFeature::PageWrite], Problem::MIRROR);
|
||||
|
||||
self::assertSame(
|
||||
['label' => 'Seiten: Nachbau von a() – x', 'kind' => 'mirror', 'features' => ['page-write']],
|
||||
$problem->toArray()
|
||||
);
|
||||
self::assertEquals($problem, Problem::fromArray($problem->toArray()));
|
||||
}
|
||||
|
||||
public function testFromArrayRefusesAnythingElse(): void
|
||||
{
|
||||
foreach ([
|
||||
'no array' => 'Seiten: x()',
|
||||
'no label' => ['kind' => 'call', 'features' => []],
|
||||
'unknown kind' => ['label' => 'x', 'kind' => 'other', 'features' => []],
|
||||
'unknown feature' => ['label' => 'x', 'kind' => 'call', 'features' => ['page-delete']],
|
||||
'features no list' => ['label' => 'x', 'kind' => 'call', 'features' => 'page-write'],
|
||||
] as $case => $row) {
|
||||
self::assertNull(Problem::fromArray($row), $case);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Abilities\WriteFailed;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviLocked;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
@@ -109,6 +110,11 @@ final class ReleaseGateTest extends TestCase
|
||||
}
|
||||
|
||||
self::assertSame(ReleaseState::Incompatible, $status->state);
|
||||
self::assertCount(count(DiviFeature::cases()), $status->matrix->unusable(), 'every feature is locked');
|
||||
self::assertSame(
|
||||
['Der Freigabe-Zustand ließ sich nicht ermitteln (Details im PHP-Error-Log).'],
|
||||
$status->matrix->of(DiviFeature::PageRead)->problems
|
||||
);
|
||||
self::assertStringContainsString('theme API gone', (string) file_get_contents($log));
|
||||
unlink($log);
|
||||
$this->expectException(DiviLocked::class);
|
||||
|
||||
@@ -4,10 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use Brain\Monkey\Filters;
|
||||
use Brain\Monkey\Functions;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\ManifestCheck;
|
||||
use LIT\MCP\Integrations\Divi\Release\Problem;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseResolver;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStore;
|
||||
use LIT\MCP\Plugin;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
@@ -15,13 +19,21 @@ use Mockery;
|
||||
|
||||
final class ReleaseResolverTest extends TestCase
|
||||
{
|
||||
private const UNKNOWN = '5.99.0';
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $transients = [];
|
||||
|
||||
private int $mirrorRuns = 0;
|
||||
|
||||
private int $codeRuns = 0;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->transients = [];
|
||||
$this->mirrorRuns = 0;
|
||||
$this->codeRuns = 0;
|
||||
Functions\when('get_transient')->alias(fn (string $key): mixed => $this->transients[$key] ?? false);
|
||||
Functions\when('set_transient')->alias(function (string $key, mixed $value): bool {
|
||||
$this->transients[$key] = $value;
|
||||
@@ -31,8 +43,9 @@ final class ReleaseResolverTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, callable(): bool> $code
|
||||
* @param array<string, callable(): bool> $mirrors
|
||||
* @param list<Problem> $code
|
||||
* @param list<Problem> $mirrors
|
||||
* @param array<string, mixed>|null $entry
|
||||
*/
|
||||
private function resolver(
|
||||
string $version,
|
||||
@@ -41,8 +54,16 @@ final class ReleaseResolverTest extends TestCase
|
||||
?array $entry = null
|
||||
): ReleaseResolver {
|
||||
$check = Mockery::mock(ManifestCheck::class);
|
||||
$check->allows('codeChecks')->andReturn($code);
|
||||
$check->allows('mirrorChecks')->andReturn($mirrors);
|
||||
$check->allows('codeProblems')->andReturnUsing(function () use ($code): array {
|
||||
++$this->codeRuns;
|
||||
|
||||
return $code;
|
||||
});
|
||||
$check->allows('mirrorProblems')->andReturnUsing(function () use ($mirrors): array {
|
||||
++$this->mirrorRuns;
|
||||
|
||||
return $mirrors;
|
||||
});
|
||||
$store = Mockery::mock(ReleaseStore::class);
|
||||
$store->allows('find')->with($version, Plugin::VERSION)->andReturn($entry);
|
||||
|
||||
@@ -54,54 +75,246 @@ final class ReleaseResolverTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testATestedVersionWithGreenChecksIsReleasedFromCode(): void
|
||||
/** @param list<DiviFeature> $features */
|
||||
private static function problem(string $kind, array $features, string $label = 'Seiten: et_x()'): Problem
|
||||
{
|
||||
return new Problem($label, $features, $kind);
|
||||
}
|
||||
|
||||
/** @return array<string, string> Feature value => state, for every feature. */
|
||||
private static function states(ReleaseStatus $status): array
|
||||
{
|
||||
$states = [];
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
$states[$feature->value] = $status->matrix->of($feature)->state->value;
|
||||
}
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $except Feature value => state.
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function allBut(string $state, array $except = []): array
|
||||
{
|
||||
$states = [];
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
$states[$feature->value] = $except[$feature->value] ?? $state;
|
||||
}
|
||||
|
||||
return $states;
|
||||
}
|
||||
|
||||
public function testATestedVersionWithoutProblemsIsReleasedFromCode(): void
|
||||
{
|
||||
$status = $this->resolver('5.2.1')->resolve();
|
||||
|
||||
self::assertSame(ReleaseState::Released, $status->state);
|
||||
self::assertSame('code', $status->source);
|
||||
self::assertSame(self::allBut('released'), self::states($status));
|
||||
self::assertSame('code', $status->matrix->of(DiviFeature::PageWrite)->source);
|
||||
}
|
||||
|
||||
public function testAnUnknownVersionIsUntestedUnlessTheDatabaseReleasedIt(): void
|
||||
public function testAReleaseCoversChangedSignaturesAndMissingReferences(): void
|
||||
{
|
||||
self::assertSame(ReleaseState::Untested, $this->resolver('5.3.0')->resolve()->state);
|
||||
$status = $this->resolver('5.2.1', [
|
||||
self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]),
|
||||
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]),
|
||||
])->resolve();
|
||||
|
||||
$released = $this->resolver('5.3.0', entry: ['user_id' => 1, 'released_at' => 'd'])->resolve();
|
||||
self::assertSame(ReleaseState::Released, $released->state);
|
||||
self::assertSame('database', $released->source);
|
||||
self::assertSame(self::allBut('released'), self::states($status));
|
||||
self::assertTrue($status->writable());
|
||||
self::assertSame([], $status->problems);
|
||||
}
|
||||
|
||||
public function testAFailedCheckMakesEvenATestedVersionIncompatible(): void
|
||||
public function testAHardProblemLocksItsFeaturesEvenForATestedVersion(): void
|
||||
{
|
||||
$status = $this->resolver('5.2.1', ['Seiten: et_x()' => static fn (): bool => false])->resolve();
|
||||
$status = $this->resolver('5.2.1', [
|
||||
self::problem(Problem::CALL, [DiviFeature::PageWrite], 'Seiten: et_x()'),
|
||||
self::problem(Problem::CONSTANT, [DiviFeature::Catalog], 'Katalog: Konstante X'),
|
||||
])->resolve();
|
||||
|
||||
self::assertSame(
|
||||
self::allBut('released', ['page-write' => 'incompatible', 'catalog' => 'incompatible']),
|
||||
self::states($status)
|
||||
);
|
||||
self::assertSame(['Seiten: et_x()'], $status->matrix->of(DiviFeature::PageWrite)->problems);
|
||||
self::assertSame(ReleaseState::Incompatible, $status->state);
|
||||
self::assertSame(['Seiten: et_x()'], $status->problems);
|
||||
self::assertSame(['Seiten: et_x()', 'Katalog: Konstante X'], $status->problems);
|
||||
self::assertFalse($status->writable());
|
||||
}
|
||||
|
||||
public function testMirrorResultsAreCachedByStateKey(): void
|
||||
public function testAProblemWithoutFeaturesLocksEveryFeature(): void
|
||||
{
|
||||
$calls = 0;
|
||||
$mirrors = ['Theme Builder: Nachbau von y()' => static function () use (&$calls): bool {
|
||||
++$calls;
|
||||
$status = $this->resolver('5.2.1', [self::problem(Problem::CALL, [])])->resolve();
|
||||
|
||||
return false;
|
||||
}];
|
||||
self::assertSame(self::allBut('incompatible'), self::states($status));
|
||||
}
|
||||
|
||||
/** Review focus 2: a changed mirror locks exactly its features – released version or automatic release. */
|
||||
public function testAChangedMirrorLocksExactlyItsFeatures(): void
|
||||
{
|
||||
$mirror = self::problem(
|
||||
Problem::MIRROR,
|
||||
[DiviFeature::ThemeBuilderWrite, DiviFeature::LibraryWrite],
|
||||
'Theme Builder: Nachbau von y() – z'
|
||||
);
|
||||
$expected = ['tb-write' => 'incompatible', 'library-write' => 'incompatible'];
|
||||
|
||||
self::assertSame(
|
||||
self::allBut('released', $expected),
|
||||
self::states($this->resolver('5.2.1', mirrors: [$mirror])->resolve())
|
||||
);
|
||||
$automatic = $this->resolver(self::UNKNOWN, mirrors: [$mirror])->resolve();
|
||||
self::assertSame(self::allBut('released', $expected), self::states($automatic));
|
||||
self::assertSame(
|
||||
['Theme Builder: Nachbau von y() – z'],
|
||||
$automatic->matrix->of(DiviFeature::ThemeBuilderWrite)->problems
|
||||
);
|
||||
}
|
||||
|
||||
public function testAnUnknownVersionWithoutProblemsIsReleasedAutomatically(): void
|
||||
{
|
||||
$status = $this->resolver(self::UNKNOWN)->resolve();
|
||||
|
||||
self::assertSame(self::allBut('released'), self::states($status));
|
||||
self::assertSame('auto', $status->source);
|
||||
self::assertSame('auto', $status->matrix->of(DiviFeature::PresetWrite)->source);
|
||||
self::assertTrue($status->writable());
|
||||
}
|
||||
|
||||
/** Review focus 3: the automatic release needs every signature unchanged. */
|
||||
public function testAChangedSignatureMakesItsFeatureIncompatibleUnderAutomaticRelease(): void
|
||||
{
|
||||
$status = $this->resolver(self::UNKNOWN, [
|
||||
self::problem(Problem::SIGNATURE, [DiviFeature::PresetWrite], 'Designsystem: x() – Signatur geändert'),
|
||||
])->resolve();
|
||||
|
||||
self::assertSame(self::allBut('released', ['preset-write' => 'incompatible']), self::states($status));
|
||||
self::assertSame(
|
||||
['Designsystem: x() – Signatur geändert'],
|
||||
$status->matrix->of(DiviFeature::PresetWrite)->problems
|
||||
);
|
||||
self::assertFalse($status->writable());
|
||||
}
|
||||
|
||||
public function testAMissingReferenceMakesItsFeatureUntestedUnderAutomaticRelease(): void
|
||||
{
|
||||
$status = $this->resolver(self::UNKNOWN, [
|
||||
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Roles], 'Seiten: r() – Referenz fehlt'),
|
||||
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::PageRead]),
|
||||
self::problem(Problem::SIGNATURE, [DiviFeature::PageRead]),
|
||||
])->resolve();
|
||||
|
||||
self::assertSame(
|
||||
self::allBut('released', ['roles' => 'untested', 'page-read' => 'incompatible']),
|
||||
self::states($status)
|
||||
);
|
||||
self::assertSame(
|
||||
['Seiten: r() – Referenz fehlt'],
|
||||
$status->matrix->of(DiviFeature::Roles)->problems
|
||||
);
|
||||
}
|
||||
|
||||
/** Review focus 1: unknown version with the automatic release switched off. */
|
||||
public function testWithoutAutomaticReleaseAnUnknownVersionIsUntestedUnlessSomethingHardFailed(): void
|
||||
{
|
||||
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->once()->with(true)->andReturn(false);
|
||||
|
||||
$status = $this->resolver(self::UNKNOWN, [
|
||||
self::problem(Problem::SIGNATURE, [DiviFeature::PresetWrite]),
|
||||
self::problem(Problem::CALL, [DiviFeature::Catalog]),
|
||||
])->resolve();
|
||||
|
||||
self::assertSame(self::allBut('untested', ['catalog' => 'incompatible']), self::states($status));
|
||||
self::assertNull($status->matrix->of(DiviFeature::PageRead)->source);
|
||||
}
|
||||
|
||||
public function testOnlyALiteralTrueSwitchesTheAutomaticReleaseOn(): void
|
||||
{
|
||||
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->once()->andReturn(1);
|
||||
|
||||
self::assertSame(self::allBut('untested'), self::states($this->resolver(self::UNKNOWN)->resolve()));
|
||||
}
|
||||
|
||||
public function testADatabaseReleaseReleasesEveryFeatureWithoutHardProblems(): void
|
||||
{
|
||||
$entry = ['user_id' => 1, 'released_at' => 'd'];
|
||||
$status = $this->resolver(self::UNKNOWN, [
|
||||
self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]),
|
||||
self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]),
|
||||
], [self::problem(Problem::MIRROR, [DiviFeature::LibraryWrite])], $entry)->resolve();
|
||||
|
||||
self::assertSame(self::allBut('released', ['library-write' => 'incompatible']), self::states($status));
|
||||
self::assertSame('database', $status->matrix->of(DiviFeature::PageWrite)->source);
|
||||
self::assertSame($entry, $status->release);
|
||||
}
|
||||
|
||||
public function testWithoutDiviNothingIsReleasedAutomatically(): void
|
||||
{
|
||||
$status = $this->resolver('')->resolve();
|
||||
|
||||
self::assertSame(ReleaseState::Untested, $status->state);
|
||||
self::assertSame(self::allBut('untested'), self::states($status));
|
||||
}
|
||||
|
||||
public function testCodeProblemsRunOnEveryResolveMirrorProblemsOncePerStateKey(): void
|
||||
{
|
||||
$mirrors = [self::problem(Problem::MIRROR, [DiviFeature::ThemeBuilderWrite], 'Theme Builder: Nachbau von y()')];
|
||||
|
||||
$first = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
|
||||
$second = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
|
||||
|
||||
self::assertSame(1, $calls, 'the second resolve reads the transient');
|
||||
self::assertSame(ReleaseState::Incompatible, $second->state);
|
||||
self::assertSame(2, $this->codeRuns, 'code and signature checks are live');
|
||||
self::assertSame(1, $this->mirrorRuns, 'the second resolve reads the transient');
|
||||
self::assertSame(self::states($first), self::states($second));
|
||||
self::assertSame($first->problems, $second->problems);
|
||||
self::assertSame('key-5.2.1', $this->transients[ReleaseResolver::CACHE]['key']);
|
||||
self::assertSame(
|
||||
['format' => 2, 'key' => 'key-5.2.1', 'problems' => [
|
||||
['label' => 'Theme Builder: Nachbau von y()', 'kind' => 'mirror', 'features' => ['tb-write']],
|
||||
]],
|
||||
$this->transients[ReleaseResolver::CACHE]
|
||||
);
|
||||
|
||||
$this->resolver('5.2.2', mirrors: $mirrors)->resolve();
|
||||
self::assertSame(2, $calls, 'another state key checks again');
|
||||
self::assertSame(2, $this->mirrorRuns, 'another state key checks again');
|
||||
}
|
||||
|
||||
public function testWithoutDiviTheVersionIsEmptyAndUntested(): void
|
||||
/** Review focus 1: an old or broken transient is never trusted. */
|
||||
public function testAnOldOrBrokenCacheEntryIsAMiss(): void
|
||||
{
|
||||
self::assertSame(ReleaseState::Untested, $this->resolver('')->resolve()->state);
|
||||
$mirrors = [self::problem(Problem::MIRROR, [DiviFeature::PageWrite])];
|
||||
$entries = [
|
||||
'format 1' => ['key' => 'key-5.2.1', 'problems' => []],
|
||||
'other format' => ['format' => 3, 'key' => 'key-5.2.1', 'problems' => []],
|
||||
'unknown feature' => ['format' => 2, 'key' => 'key-5.2.1', 'problems' => [
|
||||
['label' => 'x', 'kind' => 'mirror', 'features' => ['page-delete']],
|
||||
]],
|
||||
'no list' => ['format' => 2, 'key' => 'key-5.2.1', 'problems' => 'none'],
|
||||
'garbage' => 'x',
|
||||
];
|
||||
|
||||
foreach ($entries as $case => $entry) {
|
||||
$this->transients[ReleaseResolver::CACHE] = $entry;
|
||||
$runs = $this->mirrorRuns;
|
||||
|
||||
$status = $this->resolver('5.2.1', mirrors: $mirrors)->resolve();
|
||||
|
||||
self::assertSame($runs + 1, $this->mirrorRuns, $case);
|
||||
self::assertFalse($status->matrix->of(DiviFeature::PageWrite)->usable(), $case);
|
||||
}
|
||||
}
|
||||
|
||||
public function testTheCacheHoldsProblemsSoAReleaseCountsAtOnce(): void
|
||||
{
|
||||
Filters\expectApplied(ReleaseResolver::AUTO_RELEASE_FILTER)->andReturn(false);
|
||||
|
||||
$before = $this->resolver(self::UNKNOWN)->resolve();
|
||||
$after = $this->resolver(self::UNKNOWN, entry: ['user_id' => 1, 'released_at' => 'd'])->resolve();
|
||||
|
||||
self::assertSame(ReleaseState::Untested, $before->state);
|
||||
self::assertSame(ReleaseState::Released, $after->state);
|
||||
self::assertSame(1, $this->mirrorRuns);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Release\CompatibilityMatrix;
|
||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseState;
|
||||
use LIT\MCP\Integrations\Divi\Release\ReleaseStatus;
|
||||
use LIT\MCP\Tests\Support\ReleaseMatrices;
|
||||
use LIT\MCP\Tests\Unit\TestCase;
|
||||
use LogicException;
|
||||
|
||||
@@ -83,4 +86,50 @@ final class ReleaseStatusTest extends TestCase
|
||||
$status->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
public function testWithoutMatrixEveryFeatureIsInTheGivenState(): void
|
||||
{
|
||||
$untested = new ReleaseStatus(ReleaseState::Untested, '5.3.0');
|
||||
$broken = new ReleaseStatus(ReleaseState::Incompatible, '5.3.0', null, ['x']);
|
||||
$released = ReleaseStatus::released('5.2.1', ReleaseStatus::SOURCE_DATABASE);
|
||||
|
||||
foreach (DiviFeature::cases() as $feature) {
|
||||
self::assertSame(ReleaseState::Untested, $untested->matrix->of($feature)->state);
|
||||
self::assertSame(['x'], $broken->matrix->of($feature)->problems);
|
||||
self::assertSame('database', $released->matrix->of($feature)->source);
|
||||
}
|
||||
}
|
||||
|
||||
public function testOfTakesStateSourceAndProblemsFromTheMatrix(): void
|
||||
{
|
||||
$status = ReleaseStatus::of(
|
||||
'5.99.0',
|
||||
ReleaseMatrices::matrix([DiviFeature::Catalog], ReleaseState::Incompatible),
|
||||
['user_id' => 1]
|
||||
);
|
||||
|
||||
self::assertSame(ReleaseState::Incompatible, $status->state);
|
||||
self::assertNull($status->source);
|
||||
self::assertSame(['Seiten: et_x() – Signatur geändert'], $status->problems);
|
||||
self::assertSame(['user_id' => 1], $status->release);
|
||||
self::assertSame('auto', ReleaseStatus::of('5.99.0', CompatibilityMatrix::allReleased('auto'))->source);
|
||||
}
|
||||
|
||||
public function testWritableAsksOnlyTheWritingFeatures(): void
|
||||
{
|
||||
$readLocked = ReleaseMatrices::status([DiviFeature::Catalog, DiviFeature::PageRead]);
|
||||
self::assertSame(ReleaseState::Untested, $readLocked->state);
|
||||
self::assertTrue($readLocked->writable());
|
||||
|
||||
foreach (CompatibilityMatrix::writeFeatures() as $feature) {
|
||||
self::assertFalse(ReleaseMatrices::status([$feature])->writable(), $feature->value);
|
||||
}
|
||||
}
|
||||
|
||||
public function testAMatrixInAnotherStateThanTheStatusIsRefused(): void
|
||||
{
|
||||
$this->expectException(LogicException::class);
|
||||
|
||||
new ReleaseStatus(ReleaseState::Released, '5.2.1', 'code', [], null, CompatibilityMatrix::allIncompatible('x'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user