new Manifest( DiviAreas::PAGES, calls: [ new Call('strlen', 1), new Call('lit_mcp_missing_function', 0), new Call(ArrayObject::class . '::count', 0), new Call('Missing\Klass::run', 0), new Call('str_replace'), ], constants: ['PHP_VERSION', 'LIT_MCP_MISSING_CONSTANT'], postTypes: ['et_pb_layout'], taxonomies: ['scope'], blockTypes: ['divi/global-layout'], mirrors: [ new Mirror('same', 'a.php', 'a', null, 'gleich'), new Mirror('changed', 'b.php', 'b', null, 'geändert'), new Mirror('unreadable', 'c.php', 'c', null, 'fehlt'), new Mirror('no-expectation', 'd.php', 'd', null, 'ohne Soll'), ] ), ]; $currentHashes = ['same' => 'h1', 'changed' => 'hX', 'no-expectation' => 'h4']; return new ManifestCheck( $manifests, ['same' => 'h1', 'changed' => 'h2', 'unreadable' => 'h3'], $current ?? static fn (Mirror $mirror): ?string => $currentHashes[$mirror->id] ?? null, static fn (string $name): bool => false ); } public function testCodeChecksCoverExistenceAndArgumentCount(): void { self::assertSame( [ 'Seiten: strlen()', 'Seiten: lit_mcp_missing_function()', 'Seiten: ArrayObject::count()', 'Seiten: Klass::run()', 'Seiten: str_replace()', 'Seiten: Konstante PHP_VERSION', 'Seiten: Konstante LIT_MCP_MISSING_CONSTANT', ], array_keys($this->check()->codeChecks()) ); self::assertSame( [ 'Seiten: lit_mcp_missing_function()', 'Seiten: Klass::run()', 'Seiten: Konstante LIT_MCP_MISSING_CONSTANT', ], ManifestCheck::failed($this->check()->codeChecks()) ); } public function testAcceptsChecksTheArgumentCountTheAdapterPasses(): void { self::assertTrue(ManifestCheck::accepts(new Call('strlen', 1))); self::assertFalse(ManifestCheck::accepts(new Call('strlen', 2)), 'strlen takes one argument'); self::assertFalse(ManifestCheck::accepts(new Call('str_replace', 2)), 'str_replace needs three'); self::assertTrue(ManifestCheck::accepts(new Call('sprintf', 5)), 'variadic'); self::assertTrue(ManifestCheck::accepts(new Call('str_replace')), 'existence only'); } public function testMirrorChecksCompareWithTheExpectedFingerprints(): void { $checks = $this->check()->mirrorChecks(); self::assertSame( [ 'Seiten: Nachbau von a() – gleich', 'Seiten: Nachbau von b() – geändert', 'Seiten: Nachbau von c() – fehlt', 'Seiten: Nachbau von d() – ohne Soll', ], array_keys($checks) ); self::assertSame( [ 'Seiten: Nachbau von b() – geändert', 'Seiten: Nachbau von c() – fehlt', 'Seiten: Nachbau von d() – ohne Soll', ], ManifestCheck::failed($checks) ); } public function testUnreadableMirrorIsAFailedCheck(): void { $check = $this->check(static function (): ?string { throw new RuntimeException('Divi file gone'); }); self::assertCount(4, ManifestCheck::failed($check->mirrorChecks())); } public function testRegistrationChecksAskWordPressAndDivi(): void { Functions\when('post_type_exists')->justReturn(true); Functions\when('taxonomy_exists')->justReturn(false); $checks = $this->check()->registrationChecks(); self::assertSame( ['Seiten: Post-Typ et_pb_layout', 'Seiten: Taxonomie scope', 'Seiten: Block-Typ divi/global-layout'], array_keys($checks) ); self::assertSame( ['Seiten: Taxonomie scope', 'Seiten: Block-Typ divi/global-layout'], 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( DiviAreas::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 $problems * @return list}> */ 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') ); } public function testWithoutSignaturesACallWithoutReferenceIsNoProblem(): void { $check = new ManifestCheck( ['A' => new Manifest(DiviAreas::PAGES, calls: [new Call('strlen', 1), new Call('trim', 1)])], [], null, null, false ); self::assertSame([], $check->codeProblems()); } public function testWithoutSignaturesAMissingCallStillFails(): void { $check = new ManifestCheck( ['A' => new Manifest(DiviAreas::PAGES, calls: [new Call('strlen', 2)])], signatures: false ); self::assertSame(['call'], array_column(self::rows($check->codeProblems()), 'kind')); } public function testAMissingGlobalIsAHardProblemWithItsFeatures(): void { $GLOBALS['lit_mcp_present_global'] = 1; $GLOBALS['lit_mcp_null_global'] = null; $check = new ManifestCheck(['A' => new Manifest( DiviAreas::PAGES, globals: [ new GlobalRequirement('lit_mcp_present_global'), new GlobalRequirement('lit_mcp_null_global'), new GlobalRequirement('lit_mcp_missing_global', [DiviFeature::Catalog]), ], features: [DiviFeature::PageRead] )], signatures: false); try { $problems = $check->problems(); $failed = ManifestCheck::failed($check->codeChecks()); } finally { unset($GLOBALS['lit_mcp_present_global'], $GLOBALS['lit_mcp_null_global']); } self::assertSame( [ ['label' => 'Seiten: Globale Variable lit_mcp_null_global', 'kind' => 'global', 'features' => ['page-read']], ['label' => 'Seiten: Globale Variable lit_mcp_missing_global', 'kind' => 'global', 'features' => ['catalog']], ], self::rows($problems) ); self::assertTrue($problems[0]->isHard()); self::assertSame( ['Seiten: Globale Variable lit_mcp_null_global', 'Seiten: Globale Variable lit_mcp_missing_global'], $failed ); } public function testWithoutCurrentClosureEveryMirrorIsUnreadable(): void { $check = new ManifestCheck(['A' => new Manifest( DiviAreas::PAGES, mirrors: [new Mirror('same', 'a.php', 'a', null, 'gleich')] )], ['same' => 'h1']); self::assertSame(['mirror'], array_column(self::rows($check->mirrorProblems()), 'kind')); } public function testBlockTypesWithoutKnowsBlockClosureRefuseToBuildTheCheck(): void { $this->expectException(LogicException::class); new ManifestCheck(['A' => new Manifest(DiviAreas::PAGES, blockTypes: ['divi/x'])]); } public function testWithoutBlockTypesNoKnowsBlockClosureIsNeeded(): void { $check = new ManifestCheck(['A' => new Manifest(DiviAreas::PAGES, calls: [new Call('strlen', 1)])]); self::assertSame([], $check->registrationChecks()); } }