diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon index 56969c4d..2396ea0a 100644 --- a/Build/phpstan/phpstan.neon +++ b/Build/phpstan/phpstan.neon @@ -20,3 +20,5 @@ parameters: - ../../Classes/Core/Acceptance/* # Text fixtures extensions uses $_EXTKEY phpstan would be report as "might not defined" - ../../Tests/Unit/*/Fixtures/Extensions/*/ext_emconf.php + - ../../Tests/Unit/*/Fixtures/Packages/*/ext_emconf.php + - ../../Tests/Unit/Fixtures/Packages/*/ext_emconf.php diff --git a/Classes/Composer/ComposerPackageManager.php b/Classes/Composer/ComposerPackageManager.php index 467783b4..5df35614 100644 --- a/Classes/Composer/ComposerPackageManager.php +++ b/Classes/Composer/ComposerPackageManager.php @@ -18,8 +18,22 @@ */ use Composer\InstalledVersions; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** + * `typo3/testing-framework` internal composer package manager, used to gather source + * information of extensions already loaded by the root composer installation with + * the additional ability to register test fixture packages and extensions during + * runtime to create {@see FunctionalTestCase} test instances and provide symlinks + * of extensions into the classic mode test instance or retrieve files from a composer + * package or extension unrelated where they are placed on the filesystem. + * + * - {@see Testbase::setUpInstanceCoreLinks()} + * - {@see Testbase::linkTestExtensionsToInstance()} + * - {@see Testbase::linkFrameworkExtensionsToInstance()} + * - {@see Testbase::setUpLocalConfiguration()} + * - {@see Testbase::setUpPackageStates()} + * * @internal This class is for testing-framework internal processing and not part of public testing API. */ final class ComposerPackageManager @@ -61,28 +75,33 @@ final class ComposerPackageManager public function __construct() { + // @todo Remove this from the constructor. $this->build(); } - public function getPackageInfoWithFallback(string $name): ?PackageInfo + /** + * Get composer package information {@see PackageInfo} for `$nameOrExtensionKeyOrPath`. + */ + public function getPackageInfoWithFallback(string $nameOrExtensionKeyOrPath): ?PackageInfo { - if ($packageInfo = $this->getPackageInfo($name)) { + if ($packageInfo = $this->getPackageInfo($nameOrExtensionKeyOrPath)) { return $packageInfo; } - if ($packageInfo = $this->getPackageFromPath($name)) { + if ($packageInfo = $this->getPackageFromPath($nameOrExtensionKeyOrPath)) { return $packageInfo; } - if ($packageInfo = $this->getPackageFromPathFallback($name)) { + if ($packageInfo = $this->getPackageFromPathFallback($nameOrExtensionKeyOrPath)) { return $packageInfo; } - return null; } + /** + * Get {@see PackageInfo} for package name or extension key `$name`. + */ public function getPackageInfo(string $name): ?PackageInfo { - $name = $this->resolvePackageName($name); - return self::$packages[$name] ?? null; + return self::$packages[$this->resolvePackageName($name)] ?? null; } /** @@ -402,9 +421,23 @@ private function getExtEmConf(string $path): ?array return null; } + /** + * Returns resolved composer package name when $name is a known extension key + * for a known package, otherwise return $name unchanged. + * + * Used to determine the package name to look up as composer package within {@see self::$packages} + * + * Supports also relative classic mode notation: + * + * - typo3/sysext/backend + * - typo3conf/ext/my_ext_key + * + * {@see self::prepareResolvePackageName()} for details for normalisation. + */ private function resolvePackageName(string $name): string { - return self::$extensionKeyToPackageNameMap[$this->normalizeExtensionKey(basename($name))] ?? $name; + $name = $this->prepareResolvePackageName($name); + return self::$extensionKeyToPackageNameMap[$name] ?? $name; } /** @@ -545,11 +578,20 @@ private function determineExtensionKey( ?array $info = null, ?array $extEmConf = null ): string { - $isExtension = in_array($info['type'] ?? '', ['typo3-cms-framework', 'typo3-cms-extension'], true) - || ($extEmConf !== null); - if (!$isExtension) { + $isComposerExtensionType = ($info !== null && array_key_exists('type', $info) && is_string($info['type']) && in_array($info['type'], ['typo3-cms-framework', 'typo3-cms-extension'], true)); + $hasExtEmConf = $extEmConf !== null; + if (!($isComposerExtensionType || $hasExtEmConf)) { return ''; } + $hasComposerExtensionKey = ( + is_array($info) + && isset($info['extra']['typo3/cms']['extension-key']) + && is_string($info['extra']['typo3/cms']['extension-key']) + && $info['extra']['typo3/cms']['extension-key'] !== '' + ); + if ($hasComposerExtensionKey) { + return $info['extra']['typo3/cms']['extension-key']; + } $baseName = basename($packagePath); if (($info['type'] ?? '') === 'typo3-csm-framework' && str_starts_with($baseName, 'cms-') @@ -630,4 +672,47 @@ private function getFirstPathElement(string $path): string } return explode('/', $path)[0] ?? ''; } + + /** + * Extension can be specified with their composer name, extension key or with classic mode relative path + * prefixes (`typo3/sysext/` or `typo3conf/ext/`) for functional tests to + * configure which extension should be provided in the test instance. + * + * This method normalizes a handed over name by removing the specified extra information, so it can be + * used to resolve it either as direct package name or as extension name. + * + * Handed over value also removes known environment prefix paths, like the full path to the root (project rook), + * vendor folder or web folder using {@see self::removePrefixPaths()} which is safe, as this method is and most + * only be used for {@see self::resolvePackageName()} to find a composer package in {@see self::$packages}, after + * mapping extension-key to composer package name. + * + * Example for processed changes: + * -----------------------------_ + * + * - typo3/sysext/backend => backend + * - typo3conf/ext/my_ext_key => my_ext_key + * + * Example not processed values: + * ----------------------------- + * + * valid names + * - typo3/cms-core => typo3/cms-core + * - my-vendor/my-package-name => my-vendor/my-package-name + * - my-package-name-without-vendor => my-package-name-without-vendor + */ + private function prepareResolvePackageName($name): string + { + $name = trim($this->removePrefixPaths($name), '/'); + $relativePrefixPaths = [ + 'typo3/sysext/', + 'typo3conf/ext/', + ]; + foreach ($relativePrefixPaths as $relativePrefixPath) { + if (!str_starts_with($name, $relativePrefixPath)) { + continue; + } + $name = substr($name, mb_strlen($relativePrefixPath)); + } + return $name; + } } diff --git a/Classes/Core/Acceptance/Extension/BackendEnvironment.php b/Classes/Core/Acceptance/Extension/BackendEnvironment.php index a073b645..35d7bdad 100644 --- a/Classes/Core/Acceptance/Extension/BackendEnvironment.php +++ b/Classes/Core/Acceptance/Extension/BackendEnvironment.php @@ -21,6 +21,7 @@ use Codeception\Events; use Codeception\Extension; use TYPO3\CMS\Core\Database\ConnectionPool; +use TYPO3\CMS\Core\Information\Typo3Version; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\TestingFramework\Core\Functional\Framework\DataHandling\DataSet; use TYPO3\TestingFramework\Core\Testbase; @@ -261,8 +262,15 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent) $testbase->testDatabaseNameIsNotTooLong($originalDatabaseName, $localConfiguration); if ($dbDriver === 'mysqli' || $dbDriver === 'pdo_mysql') { $localConfiguration['DB']['Connections']['Default']['charset'] = 'utf8mb4'; - $localConfiguration['DB']['Connections']['Default']['tableoptions']['charset'] = 'utf8mb4'; - $localConfiguration['DB']['Connections']['Default']['tableoptions']['collate'] = 'utf8mb4_unicode_ci'; + if ((new Typo3Version())->getMajorVersion() >= 12) { + // @todo Use this as default when TYPO3 v11 support is dropped. + $localConfiguration['DB']['Connections']['Default']['defaultTableOptions']['charset'] = 'utf8mb4'; + $localConfiguration['DB']['Connections']['Default']['defaultTableOptions']['collation'] = 'utf8mb4_unicode_ci'; + } else { + // @todo Remove this when TYPO3 v11 support is dropped. + $localConfiguration['DB']['Connections']['Default']['tableoptions']['charset'] = 'utf8mb4'; + $localConfiguration['DB']['Connections']['Default']['tableoptions']['collate'] = 'utf8mb4_unicode_ci'; + } } } else { // sqlite dbs of all tests are stored in a dir parallel to instance roots. Allows defining this path as tmpfs. diff --git a/Classes/Core/Functional/FunctionalTestCase.php b/Classes/Core/Functional/FunctionalTestCase.php index e2c78318..15d45371 100644 --- a/Classes/Core/Functional/FunctionalTestCase.php +++ b/Classes/Core/Functional/FunctionalTestCase.php @@ -111,6 +111,23 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * * A default list of core extensions is always loaded. * + * System extension can be provided by their extension key or composer package name, + * and also as classic mode relative path + * + * ``` + * protected array $coreExensionToLoad = [ + * // As composer package name + * 'typo3/cms-core', + * // As extension-key + * 'core', + * // As relative classic mode system installation path + * 'typo3/sysext/core', + * ]; + * ``` + * + * Note that system extensions must be available, which means either added as require or + * require-dev to the root composer.json or required and installed by a required package. + * * @see FunctionalTestCaseUtility $defaultActivatedCoreExtensions * * @var non-empty-string[] @@ -121,16 +138,32 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * Array of test/fixture extensions paths that should be loaded for a test. * * This property will stay empty in this abstract, so it is possible - * to just overwrite it in extending classes. Extensions noted here will - * be loaded for every test of a test case, and it is not possible to change - * the list of loaded extensions between single tests of a test case. + * to just overwrite it in extending classes. + * + * IMPORTANT: Extension list is concrete and used to create the test instance on first + * test execution and is **NOT** changeable between single test permutations. * * Given path is expected to be relative to your document root, example: * - * array( - * 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension', + * ``` + * protected array $testExtensionToLoad = [ + * + * // Virtual relative classic mode installation path * 'typo3conf/ext/base_extension', - * ); + * + * // Virtual relative classic mode installation path subfolder test fixture + * 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension', + * + * // Relative to current test case (recommended for test fixture extension) + * __DIR__ . '/../Fixtures/Extensions/another_test_extension', + * + * // composer package name when available as `require` or `require-dev` in root composer.json + * 'vendor/some-extension', + * + * // extension key when available as package loaded as `require` or `require-dev` in root composer.json + * 'my_extension_key', + * ]; + * ``` * * Extensions in this array are linked to the test instance, loaded * and their ext_tables.sql will be applied. @@ -147,18 +180,22 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * be linked for every test of a test case, and it is not possible to change * the list of folders between single tests of a test case. * - * array( + * ``` + * protected array $pathsToLinkInTestInstance = [ * 'link-source' => 'link-destination' - * ); + * ]; + * ``` * * Given paths are expected to be relative to the test instance root. * The array keys are the source paths and the array values are the destination * paths, example: * - * [ + * ``` + * protected array $pathsToLinkInTestInstance = [ * 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' => * 'fileadmin/user_upload', - * ] + * ]; + * ``` * * To be able to link from my_own_ext the extension path needs also to be registered in * property $testExtensionsToLoad @@ -172,12 +209,14 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * paths are really duplicated and provided in the instance - instead of * using symbolic links. Examples: * - * [ + * ``` + * protected array $pathsToProvideInTestInstance = [ * // Copy an entire directory recursive to fileadmin * 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImages/' => 'fileadmin/', * // Copy a single file into some deep destination directory * 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImage/someImage.jpg' => 'fileadmin/_processed_/0/a/someImage.jpg', - * ] + * ]; + * ``` * * @var array */ @@ -208,9 +247,11 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter * To create additional folders add the paths to this array. Given paths are expected to be * relative to the test instance root and have to begin with a slash. Example: * - * [ + * ``` + * protected array $additionalFoldersToCreate = [ * 'fileadmin/user_upload' - * ] + * ]; + * ``` * * @var non-empty-string[] */ diff --git a/Classes/Core/PackageCollection.php b/Classes/Core/PackageCollection.php index cbef267e..78c65aae 100644 --- a/Classes/Core/PackageCollection.php +++ b/Classes/Core/PackageCollection.php @@ -26,10 +26,38 @@ use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\TestingFramework\Composer\ComposerPackageManager; +use TYPO3\TestingFramework\Composer\PackageInfo; +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; /** - * Collection for extension packages to resolve their dependencies in a test-base. - * Most of the code has been duplicated and adjusted from `\TYPO3\CMS\Core\Package\PackageManager`. + * Composer package collection to resolve extension dependencies for classic-mode based test instances. + * + * This class resolves extension dependencies for composer packages to sort classic-mode PackageStates, + * which only takes TYPO3 extensions into account with a fallback to read composer information when the + * `ext_emconf.php` file is missing. + * + * Most of the code has been duplicated and adjusted from {@see PackageManager}. + * + * Background: + * =========== + * + * TYPO3 has the two installation mode "composer" and "classic". For the "composer" mode the package dependency handling + * is mainly done by composer and dependency detection and sorting is purely based on composer.json information. "Classic" + * mode uses only "ext_emconf.php" information to do the same job, not mixing it with the composer.json information when + * available. + * + * Since TYPO3 v12 extensions installed in "composer" mode are not required to provide a "ext_emconf.php" anymore, which + * makes them only installable within a "composer" mode installation. Agencies used to drop that file from local path + * extensions in "composer" mode projects, because it is a not needed requirement for them and avoids maintenance of it. + * + * typo3/testing-framework builds "classic" mode functional test instances while used within composer installations only, + * and introduced an extension sorting with this class to ensure to have a deterministic extension sorting like a real + * "classic" mode installation would provide in case extensions are not manually provided in the correct order within + * {@see FunctionalTestCase::$testExtensionToLoad} property. + * + * {@see PackageCollection} is based on the TYPO3 core {@see PackageManager} to provide a sorting for functional test + * instances, falling back to use composer.json information in case no "ext_emconf.php" are given limiting it only to + * TYPO3 compatible extensions (typo3-cms-framework and typo3-cms-extension composer package types). * * @phpstan-type PackageKey non-empty-string * @phpstan-type PackageName non-empty-string @@ -54,6 +82,10 @@ public static function fromPackageStates(ComposerPackageManager $composerPackage { $packages = []; foreach ($packageStates as $packageKey => $packageStateConfiguration) { + // @todo Verify retrieving package information and throwing early exception after extension without + // composer.json support has been dropped, even for simplified test fixture extensions. Think + // about triggering deprecation for this case first, which may also breaking from a testing + // perspective. $packagePath = PathUtility::sanitizeTrailingSeparator( rtrim($basePath, '/') . '/' . $packageStateConfiguration['packagePath'] ); @@ -162,8 +194,11 @@ protected function convertConfigurationForGraph(array $allPackageConstraints, ar ]; if (isset($allPackageConstraints[$packageKey]['dependencies'])) { foreach ($allPackageConstraints[$packageKey]['dependencies'] as $dependentPackageKey) { - if (!in_array($dependentPackageKey, $packageKeys, true)) { - if ($this->isComposerDependency($dependentPackageKey)) { + $extensionKey = $this->getPackageExtensionKey($dependentPackageKey); + if (!in_array($dependentPackageKey, $packageKeys, true) + && !in_array($extensionKey, $packageKeys, true) + ) { + if (!$this->isTypo3SystemOrCustomExtension($dependentPackageKey)) { // The given package has a dependency to a Composer package that has no relation to TYPO3 // We can ignore those, when calculating the extension order continue; @@ -174,21 +209,30 @@ protected function convertConfigurationForGraph(array $allPackageConstraints, ar 1519931815 ); } - $dependencies[$packageKey]['after'][] = $dependentPackageKey; + $dependencies[$packageKey]['after'][] = $extensionKey; } } if (isset($allPackageConstraints[$packageKey]['suggestions'])) { foreach ($allPackageConstraints[$packageKey]['suggestions'] as $suggestedPackageKey) { + $extensionKey = $this->getPackageExtensionKey($suggestedPackageKey); // skip suggestions on not existing packages - if (in_array($suggestedPackageKey, $packageKeys, true)) { - // Suggestions actually have never been meant to influence loading order. - // We misuse this currently, as there is no other way to influence the loading order - // for not-required packages (soft-dependency). - // When considering suggestions for the loading order, we might create a cyclic dependency - // if the suggested package already has a real dependency on this package, so the suggestion - // has do be dropped in this case and must *not* be taken into account for loading order evaluation. - $dependencies[$packageKey]['after-resilient'][] = $suggestedPackageKey; + if (!in_array($suggestedPackageKey, $packageKeys, true) + && !in_array($extensionKey, $packageKeys, true) + ) { + continue; + } + if (!$this->isTypo3SystemOrCustomExtension($extensionKey ?: $suggestedPackageKey)) { + // Ignore non TYPO3 extension packages for suggestion determination/ordering. + continue; } + + // Suggestions actually have never been meant to influence loading order. + // We misuse this currently, as there is no other way to influence the loading order + // for not-required packages (soft-dependency). + // When considering suggestions for the loading order, we might create a cyclic dependency + // if the suggested package already has a real dependency on this package, so the suggestion + // has do be dropped in this case and must *not* be taken into account for loading order evaluation. + $dependencies[$packageKey]['after-resilient'][] = $extensionKey; } } } @@ -255,25 +299,28 @@ protected function getDependencyArrayForPackage(PackageInterface $package, array foreach ($dependentPackageConstraints as $constraint) { if ($constraint instanceof PackageConstraint) { $dependentPackageKey = $constraint->getValue(); - if (!in_array($dependentPackageKey, $dependentPackageKeys, true) && !in_array($dependentPackageKey, $trace, true)) { - $dependentPackageKeys[] = $dependentPackageKey; + $extensionKey = $this->getPackageExtensionKey($dependentPackageKey) ?: $dependentPackageKey; + if (!in_array($extensionKey, $dependentPackageKeys, true)) { + $dependentPackageKeys[] = $extensionKey; } - if (!isset($this->packages[$dependentPackageKey])) { - if ($this->isComposerDependency($dependentPackageKey)) { + + if (!isset($this->packages[$extensionKey])) { + if (!$this->isTypo3SystemOrCustomExtension($extensionKey)) { // The given package has a dependency to a Composer package that has no relation to TYPO3 // We can ignore those, when calculating the extension order continue; } + throw new Exception( sprintf( 'Package "%s" depends on package "%s" which does not exist.', $package->getPackageKey(), - $dependentPackageKey + $extensionKey ), 1695119749 ); } - $this->getDependencyArrayForPackage($this->packages[$dependentPackageKey], $dependentPackageKeys, $trace); + $this->getDependencyArrayForPackage($this->packages[$extensionKey], $dependentPackageKeys, $trace); } } return array_reverse($dependentPackageKeys); @@ -292,9 +339,17 @@ protected function getSuggestionArrayForPackage(PackageInterface $package): arra foreach ($suggestedPackageConstraints as $constraint) { if ($constraint instanceof PackageConstraint) { $suggestedPackageKey = $constraint->getValue(); - if (isset($this->packages[$suggestedPackageKey])) { - $suggestedPackageKeys[] = $suggestedPackageKey; + $extensionKey = $this->getPackageExtensionKey($suggestedPackageKey) ?: $suggestedPackageKey; + if (!$this->isTypo3SystemOrCustomExtension($suggestedPackageKey)) { + // Suggested packages which are not installed or not a TYPO3 extension can be skipped for + // sorting when not available. + continue; } + if (!isset($this->packages[$extensionKey])) { + // Suggested extension is not available in test system installation (not symlinked), ignore it. + continue; + } + $suggestedPackageKeys[] = $extensionKey; } } return array_reverse($suggestedPackageKeys); @@ -308,15 +363,38 @@ protected function findFrameworkKeys(): array $frameworkKeys = []; foreach ($this->packages as $package) { if ($package->getPackageMetaData()->isFrameworkType()) { - $frameworkKeys[] = $package->getPackageKey(); + $frameworkKeys[] = $this->getPackageExtensionKey($package->getPackageKey()) ?: $package->getPackageKey(); } } return $frameworkKeys; } - protected function isComposerDependency(string $packageKey): bool + /** + * Determines if given composer package key is either a `typo3-cms-framework` or `typo3-cms-extension` package. + */ + protected function isTypo3SystemOrCustomExtension(string $packageKey): bool + { + $packageInfo = $this->getPackageInfo($packageKey); + if ($packageInfo === null) { + return false; + } + return $packageInfo->isSystemExtension() || $packageInfo->isExtension(); + } + + /** + * Returns package extension key. Returns empty string if not available. + */ + protected function getPackageExtensionKey(string $packageKey): string + { + $packageInfo = $this->getPackageInfo($packageKey); + if ($packageInfo === null) { + return ''; + } + return $packageInfo->getExtensionKey(); + } + + protected function getPackageInfo(string $packageKey): ?PackageInfo { - $packageInfo = $this->composerPackageManager->getPackageInfo($packageKey); - return !(($packageInfo?->isSystemExtension() ?? false) || ($packageInfo?->isExtension())); + return $this->composerPackageManager->getPackageInfo($packageKey); } } diff --git a/Classes/Core/SystemEnvironmentBuilder.php b/Classes/Core/SystemEnvironmentBuilder.php index d143b7e0..43f47f73 100644 --- a/Classes/Core/SystemEnvironmentBuilder.php +++ b/Classes/Core/SystemEnvironmentBuilder.php @@ -40,16 +40,19 @@ */ class SystemEnvironmentBuilder extends CoreSystemEnvironmentBuilder { + private static ?bool $composerMode = null; + /** * @todo: Change default $requestType to 0 when dropping support for TYPO3 v12 */ - public static function run(int $entryPointLevel = 0, int $requestType = CoreSystemEnvironmentBuilder::REQUESTTYPE_FE, bool $composerMode = false) + public static function run(int $entryPointLevel = 0, int $requestType = CoreSystemEnvironmentBuilder::REQUESTTYPE_FE, ?bool $composerMode = null): void { - CoreSystemEnvironmentBuilder::run($entryPointLevel, $requestType); + self::$composerMode = $composerMode; + parent::run($entryPointLevel, $requestType); Environment::initialize( Environment::getContext(), Environment::isCli(), - $composerMode, + static::usesComposerClassLoading(), Environment::getProjectPath(), Environment::getPublicPath(), Environment::getVarPath(), @@ -58,4 +61,16 @@ public static function run(int $entryPointLevel = 0, int $requestType = CoreSyst Environment::isWindows() ? 'WINDOWS' : 'UNIX' ); } + + /** + * Manage composer mode separated from TYPO3_COMPOSER_MODE define set by typo3/cms-composer-installers. + * + * Note that this will not with earlier TYPO3 versions than 13.4. + * @link https://review.typo3.org/c/Packages/TYPO3.CMS/+/86569 + * @link https://github.com/TYPO3/testing-framework/issues/577 + */ + protected static function usesComposerClassLoading(): bool + { + return self::$composerMode ?? parent::usesComposerClassLoading(); + } } diff --git a/Classes/Core/Testbase.php b/Classes/Core/Testbase.php index e8885df9..d1cfa339 100644 --- a/Classes/Core/Testbase.php +++ b/Classes/Core/Testbase.php @@ -473,7 +473,7 @@ public function getOriginalDatabaseSettingsFromEnvironmentOrLocalConfiguration(a $originalConfigurationArray['DB']['Connections']['Default']['password'] = $databasePasswordTrimmed; } if ($databasePort) { - $originalConfigurationArray['DB']['Connections']['Default']['port'] = $databasePort; + $originalConfigurationArray['DB']['Connections']['Default']['port'] = (int)$databasePort; } if ($databaseSocket) { $originalConfigurationArray['DB']['Connections']['Default']['unix_socket'] = $databaseSocket; @@ -745,7 +745,7 @@ public function setUpBasicTypo3Bootstrap($instancePath): ContainerInterface $classLoader = require $this->getPackagesPath() . '/autoload.php'; // @todo: Remove else branch when dropping support for v12 if ($hasConsolidatedHttpEntryPoint) { - SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_CLI); + SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_CLI, false); } else { SystemEnvironmentBuilder::run(1, SystemEnvironmentBuilder::REQUESTTYPE_BE | SystemEnvironmentBuilder::REQUESTTYPE_CLI); } diff --git a/Resources/Core/Functional/Extensions/json_response/composer.json b/Resources/Core/Functional/Extensions/json_response/composer.json new file mode 100644 index 00000000..e0b15d7d --- /dev/null +++ b/Resources/Core/Functional/Extensions/json_response/composer.json @@ -0,0 +1,42 @@ +{ + "name": "typo3/testing-json-response", + "type": "typo3-cms-extension", + "description": "Providing testing framework extension for functional testing.", + "keywords": [ + "typo3", + "testing", + "tests" + ], + "homepage": "https://typo3.org/", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "TYPO3 CMS Core Team", + "role": "Developer", + "homepage": "https://forge.typo3.org/projects/typo3cms-core" + }, + { + "name": "The TYPO3 Community", + "role": "Contributor", + "homepage": "https://typo3.org/community/" + } + ], + "support": { + "general": "https://typo3.org/support/", + "issues": "https://github.com/TYPO3/testing-framework/issues" + }, + "require": { + "php": "^8.1", + "typo3/cms-core": "12.*.*@dev || 13.*.*@dev" + }, + "autoload": { + "psr-4": { + "TYPO3\\JsonResponse\\": "Classes/" + } + }, + "extra": { + "typo3/cms": { + "extension-key": "json_response" + } + } +} diff --git a/Resources/Core/Functional/Extensions/json_response/ext_emconf.php b/Resources/Core/Functional/Extensions/json_response/ext_emconf.php index 70dd550f..33519891 100644 --- a/Resources/Core/Functional/Extensions/json_response/ext_emconf.php +++ b/Resources/Core/Functional/Extensions/json_response/ext_emconf.php @@ -4,14 +4,14 @@ 'title' => 'JSON Response', 'description' => 'JSON Response', 'category' => 'example', - 'version' => '9.4.0', + 'version' => '1.0.0', 'state' => 'beta', 'author' => 'Oliver Hader', 'author_email' => 'oliver@typo3.org', 'author_company' => '', 'constraints' => [ 'depends' => [ - 'typo3' => '9.4.0', + 'typo3' => '12.0.0 - 13.9.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/Resources/Core/Functional/Extensions/private_container/composer.json b/Resources/Core/Functional/Extensions/private_container/composer.json new file mode 100644 index 00000000..f8c30d36 --- /dev/null +++ b/Resources/Core/Functional/Extensions/private_container/composer.json @@ -0,0 +1,42 @@ +{ + "name": "typo3/testing-private-container", + "type": "typo3-cms-extension", + "description": "Providing testing framework extension for functional testing.", + "keywords": [ + "typo3", + "testing", + "tests" + ], + "homepage": "https://typo3.org/", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "TYPO3 CMS Core Team", + "role": "Developer", + "homepage": "https://forge.typo3.org/projects/typo3cms-core" + }, + { + "name": "The TYPO3 Community", + "role": "Contributor", + "homepage": "https://typo3.org/community/" + } + ], + "support": { + "general": "https://typo3.org/support/", + "issues": "https://github.com/TYPO3/testing-framework/issues" + }, + "require": { + "php": "^8.1", + "typo3/cms-core": "12.*.*@dev || 13.*.*@dev" + }, + "autoload": { + "psr-4": { + "TYPO3\\PrivateContainer\\": "Classes/" + } + }, + "extra": { + "typo3/cms": { + "extension-key": "private_container" + } + } +} diff --git a/Resources/Core/Functional/Extensions/private_container/ext_emconf.php b/Resources/Core/Functional/Extensions/private_container/ext_emconf.php index c760d150..074e77b3 100644 --- a/Resources/Core/Functional/Extensions/private_container/ext_emconf.php +++ b/Resources/Core/Functional/Extensions/private_container/ext_emconf.php @@ -10,7 +10,7 @@ 'author_company' => '', 'constraints' => [ 'depends' => [ - 'typo3' => '11.0.0-12.99.99', + 'typo3' => '12.0.0-13.99.99', ], 'conflicts' => [], 'suggests' => [], diff --git a/Tests/Unit/Composer/ComposerPackageManagerTest.php b/Tests/Unit/Composer/ComposerPackageManagerTest.php index bdf4e742..8db5463e 100644 --- a/Tests/Unit/Composer/ComposerPackageManagerTest.php +++ b/Tests/Unit/Composer/ComposerPackageManagerTest.php @@ -201,8 +201,15 @@ public function coreExtensionCanBeResolvedWithRelativeLegacyPathPrefix(): void public function extensionWithoutJsonCanBeResolvedByAbsolutePath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/ext_without_composerjson_absolute'); + // Extension without composer.json registers basefolder as extension key + self::assertArrayHasKey('ext_without_composerjson_absolute', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('unknown-vendor/ext-without-composerjson-absolute', $extensionMapPropertyReflection->getValue($subject)['ext_without_composerjson_absolute']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('ext_without_composerjson_absolute', $packageInfo->getExtensionKey()); self::assertSame('unknown-vendor/ext-without-composerjson-absolute', $packageInfo->getName()); @@ -215,8 +222,15 @@ public function extensionWithoutJsonCanBeResolvedByAbsolutePath(): void public function extensionWithoutJsonCanBeResolvedRelativeFromRoot(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('Tests/Unit/Composer/Fixtures/Extensions/ext_without_composerjson_relativefromroot'); + // Extension without composer.json registers basefolder as extension key + self::assertArrayHasKey('ext_without_composerjson_relativefromroot', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('unknown-vendor/ext-without-composerjson-relativefromroot', $extensionMapPropertyReflection->getValue($subject)['ext_without_composerjson_relativefromroot']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('ext_without_composerjson_relativefromroot', $packageInfo->getExtensionKey()); self::assertSame('unknown-vendor/ext-without-composerjson-relativefromroot', $packageInfo->getName()); @@ -229,22 +243,40 @@ public function extensionWithoutJsonCanBeResolvedRelativeFromRoot(): void public function extensionWithoutJsonCanBeResolvedByLegacyPath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('typo3conf/ext/testing_framework/Tests/Unit/Composer/Fixtures/Extensions/ext_without_composerjson_fallbackroot'); + // Extension without composer.json registers basefolder as extension key + self::assertArrayHasKey('ext_without_composerjson_fallbackroot', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('unknown-vendor/ext-without-composerjson-fallbackroot', $extensionMapPropertyReflection->getValue($subject)['ext_without_composerjson_fallbackroot']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('ext_without_composerjson_fallbackroot', $packageInfo->getExtensionKey()); self::assertSame('unknown-vendor/ext-without-composerjson-fallbackroot', $packageInfo->getName()); self::assertSame('typo3-cms-extension', $packageInfo->getType()); self::assertNull($packageInfo->getInfo()); self::assertNotNull($packageInfo->getExtEmConf()); + } #[Test] public function extensionWithJsonCanBeResolvedByAbsolutePath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/ext_absolute'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_absolute', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('absolute_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-absolute', $extensionMapPropertyReflection->getValue($subject)['absolute_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('absolute_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-absolute', $packageInfo->getName()); @@ -257,8 +289,18 @@ public function extensionWithJsonCanBeResolvedByAbsolutePath(): void public function extensionWithJsonCanBeResolvedRelativeFromRoot(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('Tests/Unit/Composer/Fixtures/Extensions/ext_relativefromroot'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_relativefromroot', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('relativefromroot_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-relativefromroot', $extensionMapPropertyReflection->getValue($subject)['relativefromroot_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('relativefromroot_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-relativefromroot', $packageInfo->getName()); @@ -271,8 +313,18 @@ public function extensionWithJsonCanBeResolvedRelativeFromRoot(): void public function extensionWithJsonCanBeResolvedByLegacyPath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $packageInfo = $subject->getPackageInfoWithFallback('typo3conf/ext/testing_framework/Tests/Unit/Composer/Fixtures/Extensions/ext_fallbackroot'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_fallbackroot', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('fallbackroot_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-fallbackroot', $extensionMapPropertyReflection->getValue($subject)['fallbackroot_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('fallbackroot_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-fallbackroot', $packageInfo->getName()); @@ -285,9 +337,19 @@ public function extensionWithJsonCanBeResolvedByLegacyPath(): void public function extensionWithJsonCanBeResolvedByRelativeLegacyPath(): void { $subject = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($subject, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($subject)); $projectFolderName = basename($subject->getRootPath()); $packageInfo = $subject->getPackageInfoWithFallback('../' . $projectFolderName . '/typo3conf/ext/testing_framework/Tests/Unit/Composer/Fixtures/Extensions/ext_fallbackroot'); + // Extension with composer.json and extension key does not register basepath as extension key + self::assertArrayNotHasKey('ext_fallbackroot', $extensionMapPropertyReflection->getValue($subject)); + + // Extension with composer.json and extension key register extension key as composer package alias + self::assertArrayHasKey('fallbackroot_real', $extensionMapPropertyReflection->getValue($subject)); + self::assertSame('testing-framework/extension-fallbackroot', $extensionMapPropertyReflection->getValue($subject)['fallbackroot_real']); + + // Verify package info self::assertInstanceOf(PackageInfo::class, $packageInfo); self::assertSame('fallbackroot_real', $packageInfo->getExtensionKey()); self::assertSame('testing-framework/extension-fallbackroot', $packageInfo->getName()); @@ -295,4 +357,262 @@ public function extensionWithJsonCanBeResolvedByRelativeLegacyPath(): void self::assertNotNull($packageInfo->getInfo()); self::assertNotNull($packageInfo->getExtEmConf()); } + + public static function packagesWithoutExtEmConfFileDataProvider(): \Generator + { + yield 'package0 => package0' => [ + 'path' => __DIR__ . '/../Fixtures/Packages/package0', + 'expectedExtensionKey' => 'package0', + 'expectedPackageName' => 'typo3/testing-framework-package-0', + ]; + yield 'package0 => package1' => [ + 'path' => __DIR__ . '/../Fixtures/Packages/package1', + 'expectedExtensionKey' => 'package1', + 'expectedPackageName' => 'typo3/testing-framework-package-1', + ]; + yield 'package0 => package2' => [ + 'path' => __DIR__ . '/../Fixtures/Packages/package2', + 'expectedExtensionKey' => 'package2', + 'expectedPackageName' => 'typo3/testing-framework-package-2', + ]; + yield 'package-identifier => some_test_extension' => [ + 'path' => __DIR__ . '/../Fixtures/Packages/package-identifier', + 'expectedExtensionKey' => 'some_test_extension', + 'expectedPackageName' => 'typo3/testing-framework-package-identifier', + ]; + } + + #[DataProvider('packagesWithoutExtEmConfFileDataProvider')] + #[Test] + public function getPackageInfoWithFallbackReturnsExtensionInfoWithCorrectExtensionKeyWhenNotHavingAnExtEmConfFile( + string $path, + string $expectedExtensionKey, + string $expectedPackageName, + ): void { + $packageInfo = (new ComposerPackageManager())->getPackageInfoWithFallback($path); + self::assertInstanceOf(PackageInfo::class, $packageInfo, 'PackageInfo retrieved for ' . $path); + self::assertNull($packageInfo->getExtEmConf(), 'Package provides ext_emconf.php'); + self::assertNotNull($packageInfo->getInfo(), 'Package has no composer info (composer.json)'); + self::assertNotEmpty($packageInfo->getInfo(), 'Package composer info is empty'); + self::assertTrue($packageInfo->isExtension(), 'Package is not a extension'); + self::assertFalse($packageInfo->isSystemExtension(), 'Package is a system extension'); + self::assertTrue($packageInfo->isComposerPackage(), 'Package is not a composer package'); + self::assertFalse($packageInfo->isMonoRepository(), 'Package is mono repository'); + self::assertSame($expectedPackageName, $packageInfo->getName()); + self::assertSame($expectedExtensionKey, $packageInfo->getExtensionKey()); + } + + #[Test] + public function getPackageInfoWithFallbackReturnsExtensionInfoWithCorrectExtensionKeyAndHavingAnExtEmConfFile(): void + { + $path = __DIR__ . '/../Fixtures/Packages/package-with-extemconf'; + $expectedExtensionKey = 'extension_with_extemconf'; + $expectedPackageName = 'typo3/testing-framework-package-with-extemconf'; + $packageInfo = (new ComposerPackageManager())->getPackageInfoWithFallback($path); + self::assertInstanceOf(PackageInfo::class, $packageInfo, 'PackageInfo retrieved for ' . $path); + self::assertNotNull($packageInfo->getExtEmConf(), 'Package has ext_emconf.php file'); + self::assertNotNull($packageInfo->getInfo(), 'Package has composer info'); + self::assertNotEmpty($packageInfo->getInfo(), 'Package composer info is not empty'); + self::assertTrue($packageInfo->isExtension(), 'Package is a extension'); + self::assertFalse($packageInfo->isSystemExtension(), 'Package is not a system extension'); + self::assertTrue($packageInfo->isComposerPackage(), 'Package is a composer package'); + self::assertFalse($packageInfo->isMonoRepository(), 'Package is not mono repository root'); + self::assertSame($expectedPackageName, $packageInfo->getName()); + self::assertSame($expectedExtensionKey, $packageInfo->getExtensionKey()); + } + + public static function prepareResolvePackageNameReturnsExpectedValuesDataProvider(): \Generator + { + yield 'Composer package name returns unchanged (not checked for existence)' => [ + 'name' => 'typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Extension key returns unchanged (not checked for existence)' => [ + 'name' => 'core', + 'expected' => 'core', + ]; + yield 'Classic mode system path returns extension key (not checked for existence)' => [ + 'name' => 'typo3/sysext/core', + 'expected' => 'core', + ]; + yield 'Classic mode extension path returns extension key (not checked for existence)' => [ + 'name' => 'typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + yield 'Not existing full path to classic system extension path resolves to extension key (not checked for existence)' => [ + 'name' => 'ROOT:/typo3/sysext/core', + 'expected' => 'core', + ]; + yield 'Not existing full path to classic extension path resolves to extension key (not checked for existence)' => [ + 'name' => 'ROOT:/typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + yield 'Vendor path returns vendor with package subfolder' => [ + 'name' => 'VENDOR:/typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + } + + #[DataProvider('prepareResolvePackageNameReturnsExpectedValuesDataProvider')] + #[Test] + public function prepareResolvePackageNameReturnsExpectedValues(string $name, string $expected): void + { + $composerPackageManager = new ComposerPackageManager(); + $replaceMap = [ + 'ROOT:/' => rtrim($composerPackageManager->getRootPath(), '/') . '/', + 'VENDOR:/' => rtrim($composerPackageManager->getVendorPath(), '/') . '/', + ]; + $name = str_replace(array_keys($replaceMap), array_values($replaceMap), $name); + foreach (array_keys($replaceMap) as $replaceKey) { + self::assertStringNotContainsString($replaceKey, $name, 'Key "%s" is replaced in name "%s"'); + } + $prepareResolvePackageNameReflectionMethod = new \ReflectionMethod($composerPackageManager, 'prepareResolvePackageName'); + $resolved = $prepareResolvePackageNameReflectionMethod->invoke($composerPackageManager, $name); + self::assertSame($expected, $resolved, sprintf('"%s" resolved to "%s"', $name, $expected)); + } + + public static function resolvePackageNameReturnsExpectedPackageNameDataProvider(): \Generator + { + yield 'Composer package name returns unchanged (not checked for existence)' => [ + 'name' => 'typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Extension key returns unchanged (not checked for existence)' => [ + 'name' => 'core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Classic mode system path returns extension key (not checked for existence)' => [ + 'name' => 'typo3/sysext/core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Not existing full path to classic system extension path resolves to extension key (not checked for existence)' => [ + 'name' => 'ROOT:/typo3/sysext/core', + 'expected' => 'typo3/cms-core', + ]; + yield 'Vendor path returns vendor with package subfolder' => [ + 'name' => 'VENDOR:/typo3/cms-core', + 'expected' => 'typo3/cms-core', + ]; + // Not loaded/known extension resolves only extension key and not to a composer package name. + yield 'Not existing full path to classic extension path resolves to extension key for unknown extension' => [ + 'name' => 'ROOT:/typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + // Not loaded/known extension resolves only extension key and not to a composer package name. + yield 'Classic mode extension path returns extension key for unknown extension' => [ + 'name' => 'typo3conf/ext/some_ext', + 'expected' => 'some_ext', + ]; + } + + #[DataProvider('resolvePackageNameReturnsExpectedPackageNameDataProvider')] + #[Test] + public function resolvePackageNameReturnsExpectedPackageName(string $name, string $expected): void + { + $composerPackageManager = new ComposerPackageManager(); + $replaceMap = [ + 'ROOT:/' => rtrim($composerPackageManager->getRootPath(), '/') . '/', + 'VENDOR:/' => rtrim($composerPackageManager->getVendorPath(), '/') . '/', + ]; + $name = str_replace(array_keys($replaceMap), array_values($replaceMap), $name); + foreach (array_keys($replaceMap) as $replaceKey) { + self::assertStringNotContainsString($replaceKey, $name, 'Key "%s" is replaced in name "%s"'); + } + $resolvePackageNameReflectionMethod = new \ReflectionMethod($composerPackageManager, 'resolvePackageName'); + $resolved = $resolvePackageNameReflectionMethod->invoke($composerPackageManager, $name); + self::assertSame($expected, $resolved, sprintf('"%s" resolved to "%s"', $name, $expected)); + } + + #[Test] + public function ensureEndingComposerPackageNameAndTypoExtensionPackageExtensionKeyResolvesCorrectPackage(): void + { + $composerManager = new ComposerPackageManager(); + $extensionMapPropertyReflection = new \ReflectionProperty($composerManager, 'extensionKeyToPackageNameMap'); + self::assertIsArray($extensionMapPropertyReflection->getValue($composerManager)); + + // verify initial composer package information + $initComposerPackage = $composerManager->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Packages/sharedextensionkey'); + self::assertArrayNotHasKey('sharedextensionkey', $extensionMapPropertyReflection->getValue($composerManager)); + self::assertInstanceOf(PackageInfo::class, $initComposerPackage); + self::assertSame('testing-framework/sharedextensionkey', $initComposerPackage->getName(), 'PackageInfo->name is "testing-framework/sharedextensionkey"'); + self::assertFalse($initComposerPackage->isSystemExtension(), '"testing-framework/sharedextensionkey" is not a TYPO3 system extension'); + self::assertFalse($initComposerPackage->isExtension(), '"testing-framework/sharedextensionkey" is not a TYPO3 extension'); + self::assertTrue($initComposerPackage->isComposerPackage(), '"testing-framework/sharedextensionkey" is a composer package'); + self::assertSame('', $initComposerPackage->getExtensionKey()); + + // verify initial extension package information + $initExtensionPackage = $composerManager->getPackageInfoWithFallback(__DIR__ . '/Fixtures/Extensions/extension-key-shared-with-composer-package'); + self::assertArrayHasKey('sharedextensionkey', $extensionMapPropertyReflection->getValue($composerManager)); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $extensionMapPropertyReflection->getValue($composerManager)['sharedextensionkey']); + self::assertInstanceOf(PackageInfo::class, $initExtensionPackage); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $initExtensionPackage->getName(), 'PackageInfo->name is "testing-framework/extension-key-shared-with-composer-package"'); + self::assertFalse($initExtensionPackage->isSystemExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 system extension'); + self::assertTrue($initExtensionPackage->isExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 extension'); + self::assertTrue($initExtensionPackage->isComposerPackage(), '"testing-framework/extension-key-shared-with-composer-package" is a composer package'); + self::assertSame('sharedextensionkey', $initExtensionPackage->getExtensionKey()); + + // verify shared extension key retrieval returns the extension package + $extensionPackage = $composerManager->getPackageInfo('sharedextensionkey'); + self::assertInstanceOf(PackageInfo::class, $extensionPackage); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $extensionPackage->getName(), 'PackageInfo->name is "testing-framework/extension-key-shared-with-composer-package"'); + self::assertFalse($extensionPackage->isSystemExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 system extension'); + self::assertTrue($extensionPackage->isExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 extension'); + self::assertTrue($extensionPackage->isComposerPackage(), '"testing-framework/extension-key-shared-with-composer-package" is a composer package'); + self::assertSame('sharedextensionkey', $extensionPackage->getExtensionKey()); + + // verify shared extension key with classic mode prefix retrieval returns the extension package + $classicModeExtensionPackage = $composerManager->getPackageInfo('typo3conf/ext/sharedextensionkey'); + self::assertInstanceOf(PackageInfo::class, $classicModeExtensionPackage); + self::assertSame('testing-framework/extension-key-shared-with-composer-package', $classicModeExtensionPackage->getName(), 'PackageInfo->name is "testing-framework/extension-key-shared-with-composer-package"'); + self::assertFalse($classicModeExtensionPackage->isSystemExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 system extension'); + self::assertTrue($classicModeExtensionPackage->isExtension(), '"testing-framework/extension-key-shared-with-composer-package" is not a TYPO3 extension'); + self::assertTrue($classicModeExtensionPackage->isComposerPackage(), '"testing-framework/extension-key-shared-with-composer-package" is a composer package'); + self::assertSame('sharedextensionkey', $classicModeExtensionPackage->getExtensionKey()); + } + + /** + * @todo Remove this when fluid/standalone fluid is no longer available by default due to core dependencies. + * {@see ensureEndingComposerPackageNameAndTypoExtensionPackageExtensionKeyResolvesCorrectPackage} + */ + #[Test] + public function ensureStandaloneFluidDoesNotBreakCoreFluidExtension(): void + { + $composerManager = new ComposerPackageManager(); + + // Verify standalone fluid composer package + $standaloneFluid = $composerManager->getPackageInfo('typo3fluid/fluid'); + self::assertInstanceOf(PackageInfo::class, $standaloneFluid); + self::assertSame('typo3fluid/fluid', $standaloneFluid->getName(), 'PackageInfo->name is not "typo3fluid/fluid"'); + self::assertFalse($standaloneFluid->isSystemExtension(), '"typo3fluid/fluid" is not a TYPO3 system extension'); + self::assertFalse($standaloneFluid->isExtension(), '"typo3fluid/fluid" is not a TYPO3 extension'); + self::assertTrue($standaloneFluid->isComposerPackage(), '"typo3fluid/fluid" is a composer package'); + self::assertSame('', $standaloneFluid->getExtensionKey()); + + // Verify TYPO3 system extension fluid. + $coreFluid = $composerManager->getPackageInfo('typo3/cms-fluid'); + self::assertInstanceOf(PackageInfo::class, $coreFluid); + self::assertSame('typo3/cms-fluid', $coreFluid->getName(), 'PackageInfo->name is not "typo3/cms-fluid"'); + self::assertTrue($coreFluid->isSystemExtension(), '"typo3/cms-fluid" is a TYPO3 system extension'); + self::assertFalse($coreFluid->isExtension(), '"typo3/cms-fluid" is not a TYPO3 extension'); + self::assertTrue($coreFluid->isComposerPackage(), '"typo3/cms-fluid" is a composer package'); + self::assertSame('fluid', $coreFluid->getExtensionKey()); + + // Verify TYPO3 system extension fluid resolved using extension key. + $extensionKeyRetrievesCoreFluid = $composerManager->getPackageInfo('fluid'); + self::assertInstanceOf(PackageInfo::class, $extensionKeyRetrievesCoreFluid); + self::assertSame('typo3/cms-fluid', $extensionKeyRetrievesCoreFluid->getName(), 'PackageInfo->name is not "typo3/cms-fluid"'); + self::assertTrue($extensionKeyRetrievesCoreFluid->isSystemExtension(), '"typo3/cms-fluid" is a TYPO3 system extension'); + self::assertFalse($extensionKeyRetrievesCoreFluid->isExtension(), '"typo3/cms-fluid" is not a TYPO3 extension'); + self::assertTrue($extensionKeyRetrievesCoreFluid->isComposerPackage(), '"typo3/cms-fluid" is a composer package'); + self::assertSame('fluid', $extensionKeyRetrievesCoreFluid->getExtensionKey()); + + // Verify TYPO3 system extension fluid resolved using relative classic mode path. + $extensionRelativeSystemExtensionPath = $composerManager->getPackageInfo('typo3/sysext/fluid'); + self::assertInstanceOf(PackageInfo::class, $extensionRelativeSystemExtensionPath); + self::assertSame('typo3/cms-fluid', $extensionRelativeSystemExtensionPath->getName(), 'PackageInfo->name is not "typo3/cms-fluid"'); + self::assertTrue($extensionRelativeSystemExtensionPath->isSystemExtension(), '"typo3/cms-fluid" is a TYPO3 system extension'); + self::assertFalse($extensionRelativeSystemExtensionPath->isExtension(), '"typo3/cms-fluid" is not a TYPO3 extension'); + self::assertTrue($extensionRelativeSystemExtensionPath->isComposerPackage(), '"typo3/cms-fluid" is a composer package'); + self::assertSame('fluid', $extensionRelativeSystemExtensionPath->getExtensionKey()); + } } diff --git a/Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json b/Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json new file mode 100644 index 00000000..60065539 --- /dev/null +++ b/Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json @@ -0,0 +1,18 @@ +{ + "name": "testing-framework/extension-key-shared-with-composer-package", + "description": "TYPO3 extension shareing extension-key with last part of composer package ", + "type": "typo3-cms-extension", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Stefan Bürk", + "email": "stefan@buerk.tech" + } + ], + "require": {}, + "extra": { + "typo3/cms": { + "extension-key": "sharedextensionkey" + } + } +} diff --git a/Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json b/Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json new file mode 100644 index 00000000..de1adcd0 --- /dev/null +++ b/Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json @@ -0,0 +1,12 @@ +{ + "name": "testing-framework/sharedextensionkey", + "description": "TYPO3 extension shareing extension-key with last part of composer package ", + "license": "GPL-2.0-or-later", + "authors": [ + { + "name": "Stefan Bürk", + "email": "stefan@buerk.tech" + } + ], + "require": {} +} diff --git a/Tests/Unit/Core/PackageCollectionTest.php b/Tests/Unit/Core/PackageCollectionTest.php new file mode 100644 index 00000000..2a7bbfac --- /dev/null +++ b/Tests/Unit/Core/PackageCollectionTest.php @@ -0,0 +1,77 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +namespace Typo3\TestingFramework\Tests\Unit\Core; + +use PHPUnit\Framework\TestCase; +use TYPO3\CMS\Core\Package\PackageManager; +use TYPO3\CMS\Core\Service\DependencyOrderingService; +use TYPO3\TestingFramework\Composer\ComposerPackageManager; +use TYPO3\TestingFramework\Core\PackageCollection; + +final class PackageCollectionTest extends TestCase +{ + /** + * @test + */ + public function sortsComposerPackages(): void + { + $packageStates = require __DIR__ . '/../Fixtures/Packages/PackageStates.php'; + $expectedPackageStates = require __DIR__ . '/../Fixtures/Packages/PackageStates_sorted.php'; + $packageStates = $packageStates['packages']; + $basePath = realpath(__DIR__ . '/../../../'); + + $composerPackageManager = new ComposerPackageManager(); + // That way it knows about the extensions, this is done by TestBase upfront. + $composerPackageManager->getPackageInfoWithFallback(__DIR__ . '/../Fixtures/Packages/package0'); + $composerPackageManager->getPackageInfoWithFallback(__DIR__ . '/../Fixtures/Packages/package1'); + $composerPackageManager->getPackageInfoWithFallback(__DIR__ . '/../Fixtures/Packages/package2'); + $composerPackageManager->getPackageInfoWithFallback(__DIR__ . '/../Fixtures/Packages/package-with-extemconf'); + $composerPackageManager->getPackageInfoWithFallback(__DIR__ . '/../Fixtures/Packages/package-unsynced-extemconf'); + $composerPackageManager->getPackageInfoWithFallback(__DIR__ . '/../Fixtures/Packages/package2-unsynced-extemconf'); + + $subject = PackageCollection::fromPackageStates( + $composerPackageManager, + new PackageManager( + new DependencyOrderingService(), + __DIR__ . '/../Fixtures/Packages/PackageStates.php', + $basePath + ), + $basePath, + $packageStates + ); + + $result = $subject->sortPackageStates( + $packageStates, + new DependencyOrderingService() + ); + + self::assertSame(5, array_search('package0', array_keys($result)), 'Package 0 is not stored at loading order 5.'); + self::assertSame(6, array_search('package1', array_keys($result)), 'Package 1 is not stored at loading order 6.'); + self::assertSame(7, array_search('extension_unsynced_extemconf', array_keys($result)), 'extension_unsynced_extemconf is not stored at loading order 7.'); + self::assertSame(8, array_search('extension_with_extemconf', array_keys($result)), 'extension_with_extemconf is not stored at loading order 8.'); + self::assertSame(9, array_search('extension2_unsynced_extemconf', array_keys($result)), 'extension2_unsynced_extemconf is not stored at loading order 9.'); + self::assertSame(10, array_search('package2', array_keys($result)), 'Package 2 is not stored at loading order 10.'); + self::assertSame($expectedPackageStates['packages'], $result, 'Sorted packages does not match expected order'); + } +} diff --git a/Tests/Unit/Fixtures/Packages/PackageStates.php b/Tests/Unit/Fixtures/Packages/PackageStates.php new file mode 100644 index 00000000..77ec64de --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/PackageStates.php @@ -0,0 +1,41 @@ + [ + 'package2' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package2/', + ], + 'extbase' => [ + 'packagePath' => '.Build/vendor/typo3/cms-extbase/', + ], + 'extension2_unsynced_extemconf' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/', + ], + 'extension_unsynced_extemconf' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/', + ], + 'extension_with_extemconf' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package-with-extemconf/', + ], + 'package1' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package1/', + ], + 'fluid' => [ + 'packagePath' => '.Build/vendor/typo3/cms-fluid/', + ], + 'package0' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package0/', + ], + 'backend' => [ + 'packagePath' => '.Build/vendor/typo3/cms-backend/', + ], + 'frontend' => [ + 'packagePath' => '.Build/vendor/typo3/cms-frontend/', + ], + 'core' => [ + 'packagePath' => '.Build/vendor/typo3/cms-core/', + ], + ], + 'version' => 5, +]; diff --git a/Tests/Unit/Fixtures/Packages/PackageStates_sorted.php b/Tests/Unit/Fixtures/Packages/PackageStates_sorted.php new file mode 100644 index 00000000..8db674bb --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/PackageStates_sorted.php @@ -0,0 +1,40 @@ + [ + 'core' => [ + 'packagePath' => '.Build/vendor/typo3/cms-core/', + ], + 'extbase' => [ + 'packagePath' => '.Build/vendor/typo3/cms-extbase/', + ], + 'fluid' => [ + 'packagePath' => '.Build/vendor/typo3/cms-fluid/', + ], + 'backend' => [ + 'packagePath' => '.Build/vendor/typo3/cms-backend/', + ], + 'frontend' => [ + 'packagePath' => '.Build/vendor/typo3/cms-frontend/', + ], + 'package0' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package0/', + ], + 'package1' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package1/', + ], + 'extension_unsynced_extemconf' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/', + ], + 'extension_with_extemconf' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package-with-extemconf/', + ], + 'extension2_unsynced_extemconf' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/', + ], + 'package2' => [ + 'packagePath' => 'Tests/Unit/Fixtures/Packages/package2/', + ], + ], + 'version' => 5, +]; diff --git a/Tests/Unit/Fixtures/Packages/package-identifier/composer.json b/Tests/Unit/Fixtures/Packages/package-identifier/composer.json new file mode 100644 index 00000000..397b0898 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package-identifier/composer.json @@ -0,0 +1,17 @@ +{ + "name": "typo3/testing-framework-package-identifier", + "description": "Package 0", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/cms-core": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "some_test_extension" + } + } +} diff --git a/Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/composer.json b/Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/composer.json new file mode 100644 index 00000000..5e0ad721 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/composer.json @@ -0,0 +1,17 @@ +{ + "name": "typo3/testing-framework-package-unsynced-extemconf", + "description": "Package 0", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/cms-core": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "extension_unsynced_extemconf" + } + } +} diff --git a/Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/ext_emconf.php b/Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/ext_emconf.php new file mode 100644 index 00000000..5e62897b --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/ext_emconf.php @@ -0,0 +1,20 @@ + 'typo3/testing-framework package test extension', + 'description' => '', + 'category' => 'be', + 'state' => 'stable', + 'author' => 'TYPO3 Core Team', + 'author_email' => 'typo3cms@typo3.org', + 'author_company' => '', + 'version' => '13.4.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '13.4.0', + 'package1' => '0.0.0-9.99.99', + ], + 'conflicts' => [], + 'suggests' => [], + ], +]; diff --git a/Tests/Unit/Fixtures/Packages/package-with-extemconf/composer.json b/Tests/Unit/Fixtures/Packages/package-with-extemconf/composer.json new file mode 100644 index 00000000..d1558d42 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package-with-extemconf/composer.json @@ -0,0 +1,18 @@ +{ + "name": "typo3/testing-framework-package-with-extemconf", + "description": "Package 0", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/cms-core": "*", + "typo3/testing-framework-package-1": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "extension_with_extemconf" + } + } +} diff --git a/Tests/Unit/Fixtures/Packages/package-with-extemconf/ext_emconf.php b/Tests/Unit/Fixtures/Packages/package-with-extemconf/ext_emconf.php new file mode 100644 index 00000000..5e62897b --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package-with-extemconf/ext_emconf.php @@ -0,0 +1,20 @@ + 'typo3/testing-framework package test extension', + 'description' => '', + 'category' => 'be', + 'state' => 'stable', + 'author' => 'TYPO3 Core Team', + 'author_email' => 'typo3cms@typo3.org', + 'author_company' => '', + 'version' => '13.4.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '13.4.0', + 'package1' => '0.0.0-9.99.99', + ], + 'conflicts' => [], + 'suggests' => [], + ], +]; diff --git a/Tests/Unit/Fixtures/Packages/package0/composer.json b/Tests/Unit/Fixtures/Packages/package0/composer.json new file mode 100644 index 00000000..96c0eab4 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package0/composer.json @@ -0,0 +1,17 @@ +{ + "name": "typo3/testing-framework-package-0", + "description": "Package 0", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/cms-core": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "package0" + } + } +} diff --git a/Tests/Unit/Fixtures/Packages/package1/composer.json b/Tests/Unit/Fixtures/Packages/package1/composer.json new file mode 100644 index 00000000..a4131ac8 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package1/composer.json @@ -0,0 +1,21 @@ +{ + "name": "typo3/testing-framework-package-1", + "description": "Package 1, with replace entry", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/cms-core": "*", + "typo3/testing-framework-package-0": "*" + }, + "replace": { + "typo3-ter/package1": "self.version" + }, + "extra": { + "typo3/cms": { + "extension-key": "package1" + } + } +} diff --git a/Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/composer.json b/Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/composer.json new file mode 100644 index 00000000..befbcc8b --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/composer.json @@ -0,0 +1,18 @@ +{ + "name": "typo3/testing-framework-package2-unsynced-extemconf", + "description": "Package 0", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/cms-core": "*", + "typo3/testing-framework-package-1": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "extension2_unsynced_extemconf" + } + } +} diff --git a/Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/ext_emconf.php b/Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/ext_emconf.php new file mode 100644 index 00000000..4e84ca09 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/ext_emconf.php @@ -0,0 +1,19 @@ + 'typo3/testing-framework package test extension', + 'description' => '', + 'category' => 'be', + 'state' => 'stable', + 'author' => 'TYPO3 Core Team', + 'author_email' => 'typo3cms@typo3.org', + 'author_company' => '', + 'version' => '13.4.0', + 'constraints' => [ + 'depends' => [ + 'typo3' => '13.4.0', + ], + 'conflicts' => [], + 'suggests' => [], + ], +]; diff --git a/Tests/Unit/Fixtures/Packages/package2/composer.json b/Tests/Unit/Fixtures/Packages/package2/composer.json new file mode 100644 index 00000000..967c3c57 --- /dev/null +++ b/Tests/Unit/Fixtures/Packages/package2/composer.json @@ -0,0 +1,23 @@ +{ + "name": "typo3/testing-framework-package-2", + "description": "Package 2 depending on package 1", + "type": "typo3-cms-extension", + "license": [ + "GPL-2.0-or-later" + ], + "require": { + "php": "*", + "typo3/testing-framework-package-1": "*", + "typo3/testing-framework-package-0": "*", + "typo3/testing-framework-package-with-extemconf": "*", + "typo3/testing-framework-package-with-extemconf": "*", + "typo3/testing-framework-package-unsynced-extemconf": "*", + "typo3/testing-framework-package2-unsynced-extemconf": "*", + "typo3/cms-core": "*" + }, + "extra": { + "typo3/cms": { + "extension-key": "package2" + } + } +}