> */ public function testMainCallmapFileContainsACallmap(string $callMapFilePath): array { /** * @var array> * @psalm-suppress UnresolvableInclude */ $mainCallMap = include($callMapFilePath); /** @psalm-suppress RedundantConditionGivenDocblockType */ self::assertIsArray($mainCallMap, "Main CallMap file " . $callMapFilePath . " does not contain a readable array"); return $mainCallMap; } /** * @depends testDictionaryPathMustBeAReadableDirectory * @return array>, * changed: array, * new: array * }>, * removed: array> * }> */ public function testDeltaFilesContainAddedChangedAndRemovedSections(): array { /** @var iterable */ $deltaFileIterator = new RegexIterator( new FilesystemIterator( self::DICTIONARY_PATH, FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::SKIP_DOTS, ), '/^CallMap_[\d]{2,}_delta\.php$/i', RegexIterator::MATCH, RegexIterator::USE_KEY, ); $deltaFiles = []; foreach ($deltaFileIterator as $deltaFile => $deltaFilePath) { if (!is_file($deltaFilePath)) { continue; } /** * @var array{ * added: array>, * changed: array, * new: array * }>, * removed: array> * } */ $deltaFiles[$deltaFile] = include($deltaFilePath); } uksort($deltaFiles, 'strnatcasecmp'); $deltaFiles = [ 'CallMap_historical.php' => [ 'added' => include 'dictionaries/CallMap_historical.php', 'changed' => [], 'removed' => [], ], ] + $deltaFiles; foreach ($deltaFiles as $name => $deltaFile) { /** @psalm-suppress RedundantConditionGivenDocblockType */ self::assertIsArray($deltaFile, "Delta file " . $name . " doesn't contain a readable array"); self::assertArrayHasKey('added', $deltaFile, "Delta file " . $name . " has no 'added' section"); self::assertArrayHasKey('changed', $deltaFile, "Delta file " . $name . " has no 'changed' section"); self::assertArrayHasKey('removed', $deltaFile, "Delta file " . $name . " has no 'removed' section"); /** @psalm-suppress RedundantCondition */ self::assertIsArray($deltaFile['added'], "'added' section in Delta file " . $name . " doesn't contain a readable array"); /** @psalm-suppress RedundantCondition */ self::assertIsArray($deltaFile['removed'], "'removed' section in Delta file " . $name . " doesn't contain a readable array"); /** @psalm-suppress RedundantCondition */ self::assertIsArray($deltaFile['changed'], "'changed' section in Delta file " . $name . " doesn't contain a readable array"); } return $deltaFiles; } /** * @depends testMainCallmapFileContainsACallmap * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @param array> $mainCallMap * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles */ public function testCallmapKeysAreStringsAndValuesAreSignatures(array $mainCallMap, array $deltaFiles): void { self::assertArrayKeysAreStrings($mainCallMap, "Main CallMap has non-string keys"); self::assertArrayValuesAreArrays($mainCallMap, "Main CallMap has non-array values"); foreach ($deltaFiles as $name => $deltaFile) { foreach (['added', 'changed', 'removed'] as $section) { self::assertArrayKeysAreStrings($deltaFile[$section], "'" . $section . "' in delta file " . $name . " has non-string keys"); self::assertArrayValuesAreArrays($deltaFile[$section], "'" . $section . "' in delta file " . $name . " has non-array values"); } foreach ($deltaFile['changed'] as $changedFunction => $diff) { self::assertArrayKeysAreStrings($diff, "Changed function " . $changedFunction . "in delta file " . $name . " has non-string keys"); self::assertArrayValuesAreArrays($diff, "Changed function " . $changedFunction . "in delta file " . $name . " has non-array values"); foreach (['old', 'new'] as $section) { self::assertArrayHasKey($section, $diff, "Changed function " . $changedFunction . "in delta file " . $name . " has no '" . $section . "' section"); } } } } /** * @depends testMainCallmapFileContainsACallmap * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @depends testCallmapKeysAreStringsAndValuesAreSignatures * @param array> $mainCallMap * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles */ public function testSignatureKeysAreZeroOrStringAndValuesAreTypes(array $mainCallMap, array $deltaFiles): void { foreach ($mainCallMap as $function => $signature) { self::assertArrayKeysAreZeroOrString($signature, "Function " . $function . " in main CallMap has invalid keys"); self::assertArrayValuesAreStrings($signature, "Function " . $function . " in main CallMap has non-string values"); } foreach ($deltaFiles as $name => $deltaFile) { foreach (['added', 'removed'] as $section) { foreach ($deltaFile[$section] as $function => $signature) { self::assertArrayKeysAreZeroOrString($signature, "Function " . $function . " in '" . $section . "' of delta file " . $name . " has invalid keys"); self::assertArrayValuesAreStrings($signature, "Function " . $function . " in '" . $section . "' of delta file " . $name . " has non-string values"); } } foreach ($deltaFile['changed'] as $function => $diff) { foreach (['old', 'new'] as $section) { self::assertArrayKeysAreZeroOrString( $diff[$section], "'" . $section . "' function " . $function . " in 'changed' of delta file " . $name . " has invalid keys", ); self::assertArrayValuesAreStrings( $diff[$section], "'" . $section . "' function " . $function . " in 'changed' of delta file " . $name . " has non-string values", ); } } } } /** * @depends testMainCallmapFileContainsACallmap * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @depends testSignatureKeysAreZeroOrStringAndValuesAreTypes * @param array> $mainCallMap * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles */ public function testTypesAreParsable(array $mainCallMap, array $deltaFiles): void { foreach ($mainCallMap as $function => $signature) { foreach ($signature as $type) { self::assertStringIsParsableType($type, "Function " . $function . " in main CallMap contains invalid type declaration " . $type); } } foreach ($deltaFiles as $name => $deltaFile) { foreach (['added', 'removed'] as $section) { foreach ($deltaFile[$section] as $function => $signature) { foreach ($signature as $type) { self::assertStringIsParsableType( $type, "Function " . $function . " in '" . $section . "' of delta file " . $name . " contains invalid type declaration " . $type, ); } } } foreach ($deltaFile['changed'] as $function => $diff) { foreach (['old', 'new'] as $section) { foreach ($diff[$section] as $type) { self::assertStringIsParsableType( $type, "'" . $section . "' function " . $function . " in 'changed' of delta file " . $name . " contains invalid type declaration " . $type, ); } } } } } /** * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @depends testCallmapKeysAreStringsAndValuesAreSignatures * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles * @return list */ public function testChangedAndRemovedFunctionsMustExist(array $deltaFiles): array { $newFunctions = []; $deletedFunctions = []; foreach ($deltaFiles as $name => $deltaFile) { $addedFunctions = array_keys($deltaFile['added']); $removedFunctions = array_keys($deltaFile['removed']); $nonExistingChangedFunctions = array_diff(array_keys($deltaFile['changed']), $newFunctions); $nonExistingRemovedFunctions = array_diff($removedFunctions, $newFunctions); self::assertEquals( array_values($nonExistingChangedFunctions), [], // Compare against empty array to get handy diff in output "Deltafile " . $name . " tries to change non-existing functions", ); self::assertEquals( array_values($nonExistingRemovedFunctions), [], // Compare against empty array to get handy diff in output "Deltafile " . $name . " tries to remove non-existing functions", ); $newFunctions = array_diff($newFunctions, $removedFunctions); $newFunctions = [...$newFunctions, ...$addedFunctions]; $deletedFunctions = array_diff($deletedFunctions, $addedFunctions); $deletedFunctions = [...$deletedFunctions, ...$removedFunctions]; } return $deletedFunctions; } /** * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @depends testCallmapKeysAreStringsAndValuesAreSignatures * @depends testChangedAndRemovedFunctionsMustExist * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles * @return array> */ public function testExistingFunctionsCanNotBeAdded(array $deltaFiles): array { $newFunctions = []; foreach ($deltaFiles as $name => $deltaFile) { $alreadyExistingFunctions = array_intersect_key($deltaFile['added'], $newFunctions); self::assertEquals( array_values($alreadyExistingFunctions), [], // Compare against empty array to get handy diff in output "Deltafile " . $name . " adds already existing functions", ); $newFunctions = array_diff_key($newFunctions, $deltaFile['removed']); foreach ($deltaFile['changed'] as $function => ['new' => $new]) { $newFunctions[$function] = $new; } $newFunctions = array_merge($newFunctions, $deltaFile['added']); } return $newFunctions; } /** * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @depends testCallmapKeysAreStringsAndValuesAreSignatures * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles */ public function testFunctionsCanNotBeInMoreThanOneSection(array $deltaFiles): void { foreach ($deltaFiles as $name => $deltaFile) { $addedFunctions = array_keys($deltaFile['added']); $changedFunctions = array_keys($deltaFile['changed']); $removedFunctions = array_keys($deltaFile['removed']); $overlapAddedChanged = array_intersect($addedFunctions, $changedFunctions); $overlapAddedRemoved = array_intersect($addedFunctions, $removedFunctions); $overlapChangedRemoved = array_intersect($changedFunctions, $removedFunctions); self::assertEquals( array_values($overlapAddedChanged), [], // Compare against empty array to get handy diff in output "Deltafile " . $name . " adds and changes the same functions", ); self::assertEquals( array_values($overlapAddedRemoved), [], // Compare against empty array to get handy diff in output "Deltafile " . $name . " adds and removes the same functions. Move them to the 'changed' section", ); self::assertEquals( array_values($overlapChangedRemoved), [], // Compare against empty array to get handy diff in output "Deltafile " . $name . " changes and removes the same function", ); } } /** * @depends testMainCallmapFileContainsACallmap * @depends testExistingFunctionsCanNotBeAdded * @depends testCallmapKeysAreStringsAndValuesAreSignatures * @param array> $mainCallMap * @param array> $newFunctions */ public function testFunctionsAddedInDeltaFilesArePresentInMainCallmap(array $mainCallMap, array $newFunctions): array { $missingNewFunctions = array_diff(array_keys($newFunctions), array_keys($mainCallMap)); self::assertEquals( array_values($missingNewFunctions), [], // Compare against empty array to get handy diff in output "Not all functions added in delta files are present in main CallMap file", ); return $newFunctions; } /** * @depends testMainCallmapFileContainsACallmap * @depends testExistingFunctionsCanNotBeAdded * @depends testCallmapKeysAreStringsAndValuesAreSignatures * @param array> $mainCallMap * @param array> $newFunctions */ public function testFunctionsPresentInMainCallmapAreAddedInDeltaFiles(array $mainCallMap, array $newFunctions): void { $strayNewFunctions = array_diff(array_keys($mainCallMap), array_keys($newFunctions)); self::assertEquals( [], // Compare against empty array to get handy diff in output array_values($strayNewFunctions), "Not all functions present in main CallMap file are added in delta files", ); } /** * @depends testMainCallmapFileContainsACallmap * @depends testChangedAndRemovedFunctionsMustExist * @depends testSignatureKeysAreZeroOrStringAndValuesAreTypes * @param array> $mainCallMap * @param list $removedFunctions */ public function testFunctionsRemovedInDeltaFilesAreAbsentFromMainCallmap(array $mainCallMap, array $removedFunctions): void { $stillPresentRemovedFunctions = array_intersect($removedFunctions, array_keys($mainCallMap)); self::assertEquals( [], // Compare against empty array to get handy diff in output array_values($stillPresentRemovedFunctions), "Not all functions removed in delta files are absent in main CallMap file", ); } /** * @depends testMainCallmapFileContainsACallmap * @depends testFunctionsAddedInDeltaFilesArePresentInMainCallmap * @depends testFunctionsPresentInMainCallmapAreAddedInDeltaFiles * @param array> $mainCallMap * @param array> $newFunctions */ public function testMainCallmapSignaturesMustMatchMostRecentIncomingSignatures(array $mainCallMap, array $newFunctions): void { $existingFunctions = array_intersect_key($mainCallMap, $newFunctions); ksort($existingFunctions); ksort($newFunctions); self::assertEquals( $newFunctions, $existingFunctions, "Signatures in CallMap file don't match most recent signatures in delta files", ); } /** * @depends testDeltaFilesContainAddedChangedAndRemovedSections * @depends testSignatureKeysAreZeroOrStringAndValuesAreTypes * @param array>, * changed: array, * new: array * }>, * removed: array> * }> $deltaFiles */ public function testOutgoingSignaturesMustMatchMostRecentIncomingSignatures(array $deltaFiles): void { $deltaFileNames = array_keys($deltaFiles); for ($i = count($deltaFileNames) - 1; $i > 0; $i--) { $outgoingSignatures = $deltaFiles[$deltaFileNames[$i]]['removed']; foreach ($deltaFiles[$deltaFileNames[$i]]['changed'] as $function => ['old' => $old]) { $outgoingSignatures[$function] = $old; } ksort($outgoingSignatures); for ($j = $i - 1; $j >= 0; $j--) { $incomingSignatures = $deltaFiles[$deltaFileNames[$j]]['added']; foreach ($deltaFiles[$deltaFileNames[$j]]['changed'] as $function => ['new' => $new]) { $incomingSignatures[$function] = $new; } ksort($incomingSignatures); $overlapOutgoing = array_intersect_key($outgoingSignatures, $incomingSignatures); if (count($overlapOutgoing) !== 0) { $overlapIncoming = array_intersect_key($incomingSignatures, $outgoingSignatures); self::assertEquals( $overlapOutgoing, $overlapIncoming, "Outgoing signatures in " . $deltaFileNames[$i] . " don't match corresponding incoming signatures in " . $deltaFileNames[$j], ); // Don't check what has already been matched $outgoingSignatures = array_diff_key($outgoingSignatures, $overlapOutgoing); } } } } }