*/ 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; return true; }); } /** * @param list $code * @param list $mirrors * @param array|null $entry */ private function resolver( string $version, array $code = [], array $mirrors = [], ?array $entry = null ): ReleaseResolver { $check = Mockery::mock(ManifestCheck::class); $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); return new ReleaseResolver( $check, $store, static fn (): ?string => $version, static fn (string $v): string => 'key-' . $v ); } /** @param list $features */ private static function problem(string $kind, array $features, string $label = 'Seiten: et_x()'): Problem { return new Problem($label, $features, $kind); } /** @return array 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 $except Feature value => state. * @return array */ 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 testAReleaseCoversChangedSignaturesAndMissingReferences(): void { $status = $this->resolver('5.2.1', [ self::problem(Problem::SIGNATURE, [DiviFeature::PageWrite]), self::problem(Problem::REFERENCE_MISSING, [DiviFeature::Catalog]), ])->resolve(); self::assertSame(self::allBut('released'), self::states($status)); self::assertTrue($status->writable()); self::assertSame([], $status->problems); } public function testAHardProblemLocksItsFeaturesEvenForATestedVersion(): void { $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()', 'Katalog: Konstante X'], $status->problems); self::assertFalse($status->writable()); } public function testAProblemWithoutFeaturesLocksEveryFeature(): void { $status = $this->resolver('5.2.1', [self::problem(Problem::CALL, [])])->resolve(); 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(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( ['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, $this->mirrorRuns, 'another state key checks again'); } /** Review focus 1: an old or broken transient is never trusted. */ public function testAnOldOrBrokenCacheEntryIsAMiss(): void { $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); } }