From a6aa622fd7639fd535f3edb5ac9bc83323671939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Wed, 27 Nov 2024 17:59:12 +0100 Subject: [PATCH 1/5] [TASK] Add missing `composer.json` to testing-framework extensions The typo3/testing-framework provides two TYPO3 extensions, which are required and bound to every created functional test instance. Adding missing `composer.json` for these two extensions. Releases: main, 8 --- .../Extensions/json_response/composer.json | 42 +++++++++++++++++++ .../Extensions/json_response/ext_emconf.php | 4 +- .../private_container/composer.json | 42 +++++++++++++++++++ .../private_container/ext_emconf.php | 2 +- 4 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 Resources/Core/Functional/Extensions/json_response/composer.json create mode 100644 Resources/Core/Functional/Extensions/private_container/composer.json 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..42283d4b --- /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.2", + "typo3/cms-core": "13.*.*@dev || 14.*.*@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..424d5ac8 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' => '13.0.0 - 14.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..a92a36df --- /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.2", + "typo3/cms-core": "13.*.*@dev || 14.*.*@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..f20a6540 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' => '13.0.0-14.99.99', ], 'conflicts' => [], 'suggests' => [], From c3d94c469846364473326171ca1e3e39062b0ecd Mon Sep 17 00:00:00 2001 From: Daniel Siepmann Date: Wed, 14 Feb 2024 13:15:33 +0100 Subject: [PATCH 2/5] [BUGFIX] Respect `composer mode only` extension in `FunctionalTestCase` The `typo3/testing-framework` creates functional test instances as "classic" mode instances, writing a `PackageStates.php` file composed using composer information, provided and handled by the internal `ComposerPackageManager` class. Extensions in the state file are not sorted based on their dependencies and suggestions. To mitigate this and having a correctly sorted extension state file, the `PackageCollection` service has been introduced with the goal to resort the extension state file after the initial write to provide the correct sorted extension state. The extension sorting within the state file is important, as the TYPO3 Core uses this sorting to loop over extensions to collect information from the extensions, for example TCA, TCAOverrides, ext_localconf.php and other things. Package sorting is here very important to allow extensions to reconfigure or change the stuff from other extensions, which is guaranteed in normal "composer" and "classic" mode instances. For "classic" mode instances, only the `ext_emconf.php` file is taken into account and "composer.json" as the source of thruth for "composer" mode instances since TYPO3 v12, which made the `ext_emconf.php` file obsolete for extensions only installed with composer. Many agencies removed the optional `ext_emconf.php` file for project local path extensions like a sitepackage to remove the maintenance burden, which is a valid case. Sadly, the `PackageCollection` adopted from the TYPO3 Core `PackageManager` did not reflected this case and failed for extensions to properly sort only on extension dependencies and keys due the mix of extension key and composer package name handling. Extension depending on another extension failed to be sorted correctly with following exception: UnexpectedValueException: The package "extension_key" depends on "composer/package-key" which is not present in the system. This change modifies the `PackageCollection` implementation to take only TYPO3 extension and system extension into account for dependency resolving and sorting, using `ext_emconf.php` depends/suggest information as first source and falling back to `composer` require and suggestion information. Resolves: #541 Releases: main, 8 --- Build/phpstan/phpstan.neon | 2 + Classes/Composer/ComposerPackageManager.php | 16 ++- Classes/Core/PackageCollection.php | 128 ++++++++++++++---- .../Composer/ComposerPackageManagerTest.php | 63 +++++++++ Tests/Unit/Core/PackageCollectionTest.php | 77 +++++++++++ .../Unit/Fixtures/Packages/PackageStates.php | 41 ++++++ .../Packages/PackageStates_sorted.php | 40 ++++++ .../Packages/package-identifier/composer.json | 17 +++ .../package-unsynced-extemconf/composer.json | 17 +++ .../package-unsynced-extemconf/ext_emconf.php | 20 +++ .../package-with-extemconf/composer.json | 18 +++ .../package-with-extemconf/ext_emconf.php | 20 +++ .../Fixtures/Packages/package0/composer.json | 17 +++ .../Fixtures/Packages/package1/composer.json | 21 +++ .../package2-unsynced-extemconf/composer.json | 18 +++ .../ext_emconf.php | 19 +++ .../Fixtures/Packages/package2/composer.json | 23 ++++ 17 files changed, 529 insertions(+), 28 deletions(-) create mode 100644 Tests/Unit/Core/PackageCollectionTest.php create mode 100644 Tests/Unit/Fixtures/Packages/PackageStates.php create mode 100644 Tests/Unit/Fixtures/Packages/PackageStates_sorted.php create mode 100644 Tests/Unit/Fixtures/Packages/package-identifier/composer.json create mode 100644 Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/composer.json create mode 100644 Tests/Unit/Fixtures/Packages/package-unsynced-extemconf/ext_emconf.php create mode 100644 Tests/Unit/Fixtures/Packages/package-with-extemconf/composer.json create mode 100644 Tests/Unit/Fixtures/Packages/package-with-extemconf/ext_emconf.php create mode 100644 Tests/Unit/Fixtures/Packages/package0/composer.json create mode 100644 Tests/Unit/Fixtures/Packages/package1/composer.json create mode 100644 Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/composer.json create mode 100644 Tests/Unit/Fixtures/Packages/package2-unsynced-extemconf/ext_emconf.php create mode 100644 Tests/Unit/Fixtures/Packages/package2/composer.json 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 86a90483..deadac19 100644 --- a/Classes/Composer/ComposerPackageManager.php +++ b/Classes/Composer/ComposerPackageManager.php @@ -61,6 +61,7 @@ final class ComposerPackageManager public function __construct() { + // @todo Remove this from the constructor. $this->build(); } @@ -545,11 +546,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-') diff --git a/Classes/Core/PackageCollection.php b/Classes/Core/PackageCollection.php index b25018b3..1604a002 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'] ); @@ -157,8 +189,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; @@ -169,21 +204,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; } } } @@ -250,25 +294,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); @@ -287,9 +334,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); @@ -303,15 +358,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/Tests/Unit/Composer/ComposerPackageManagerTest.php b/Tests/Unit/Composer/ComposerPackageManagerTest.php index bdf4e742..84d2a67a 100644 --- a/Tests/Unit/Composer/ComposerPackageManagerTest.php +++ b/Tests/Unit/Composer/ComposerPackageManagerTest.php @@ -295,4 +295,67 @@ 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()); + } } 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" + } + } +} From 1530ae5dc491d7b4338bf526f5fed9fcb91338a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Thu, 28 Nov 2024 11:17:30 +0100 Subject: [PATCH 3/5] [BUGFIX] Avoid resolving invalid TYPO3 extensions in `ComposerPackageManager` The `ComposerPackageManager` has been introduced to streamline the functional test instance creation process and provide all selected extensions (system, custom and test fixture) within the test instance, which original simply used relative path names for classic mode instances or a simple extension key: For `$coreExtensionsToLoad`: * typo3/sysext/backend * backend For `$testExtensionsToLoad`: * typo3conf/ext/my_ext_key * my_ext_key With `typo3/cms-composer-installers` version 4.0RC1 and 5 these paths could not be found anymore, because TYPO3 system extensions and extensions are no longer installed into the classic paths in a composer mode instance and left in the vendor folder, which is the case for usual root project or extension instance. Using the available composer information to determine the source for extensions unrelated to the real installation path, which can be configured with composer, was the way to mitigate this issue and `ComposerPackageManger` has been implemented to process these lookups while still supporting test fixture extensions not loaded by the root composer.json directly. The implementation tried to provide backwards compatible as much as possible along with fallback to use folder names as extension keys by simply using `basename()` in some code places and including not obvious side effects and lookup issues. `basename()` was also used on valid composer package names to resolve composer package name for that value as extension key for loaded packages, which leads to fetch the wrong composer package. The standlone fluid `typo3fluid/fluid` composer package name resolved and retrieved the TYPO3 system extension package `typo3/cms-fluid` using `getPackageInfo()`, which lead to issues in other places, for example the dependency ordering and resolving class `PackageCollection`. This change streamlines the `ComposerPackageManager` class to mitigate building and using invalid values to lookup extension composer package names and harden the registration process of extension even further. Guarding unit tests are added to cover this bugfix. Resolves: #553 Releases: main, 8 --- Classes/Composer/ComposerPackageManager.php | 91 ++++++- .../Core/Functional/FunctionalTestCase.php | 69 ++++- .../Composer/ComposerPackageManagerTest.php | 257 ++++++++++++++++++ .../composer.json | 18 ++ .../Packages/sharedextensionkey/composer.json | 12 + 5 files changed, 425 insertions(+), 22 deletions(-) create mode 100644 Tests/Unit/Composer/Fixtures/Extensions/extension-key-shared-with-composer-package/composer.json create mode 100644 Tests/Unit/Composer/Fixtures/Packages/sharedextensionkey/composer.json diff --git a/Classes/Composer/ComposerPackageManager.php b/Classes/Composer/ComposerPackageManager.php index deadac19..7f0db5b5 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 @@ -65,25 +79,29 @@ public function __construct() $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; } /** @@ -403,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; } /** @@ -640,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/Functional/FunctionalTestCase.php b/Classes/Core/Functional/FunctionalTestCase.php index c755344f..bd744497 100644 --- a/Classes/Core/Functional/FunctionalTestCase.php +++ b/Classes/Core/Functional/FunctionalTestCase.php @@ -108,6 +108,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[] @@ -118,16 +135,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. @@ -144,18 +177,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 @@ -169,12 +206,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 */ @@ -205,9 +244,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/Tests/Unit/Composer/ComposerPackageManagerTest.php b/Tests/Unit/Composer/ComposerPackageManagerTest.php index 84d2a67a..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()); @@ -358,4 +420,199 @@ public function getPackageInfoWithFallbackReturnsExtensionInfoWithCorrectExtensi 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": {} +} From 18329b03c2b11f34a2fbca916891b052833983c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Thu, 28 Nov 2024 21:03:25 +0100 Subject: [PATCH 4/5] [BUGFIX] Avoid TypeError for database port handling Using the `typo3DatabasePort` environment variable to define the database server port string-casts the value to an string which is not compatible with the mysqli method `mysqli::real_connect()` and fails with: TypeError: mysqli::real_connect(): Argument #5 ($port) must be of type ?int, string given This change casts the database port to an integer in case a port is provided to avoid PHP TypeError when passing to `mysqli::real_connect()` to align with `ConnectionPool`, which is not used to create the database for the functional test instance. Resolves: #631 Releases: main, 8 --- Classes/Core/Testbase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Core/Testbase.php b/Classes/Core/Testbase.php index 4b848ccf..9ac06333 100644 --- a/Classes/Core/Testbase.php +++ b/Classes/Core/Testbase.php @@ -453,7 +453,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; From 32cc77242e0b0c2e76810e6a698d1ca4313740cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20B=C3=BCrk?= Date: Fri, 29 Nov 2024 09:58:21 +0100 Subject: [PATCH 5/5] [TASK] Do not enable feature `redirects.hitCount` for acceptance tests This change removes the enabling of feature `redirects.hitCount` within codeception acceptance test setups to stay on TYPO3 Core defaults. That has been preared with temporary solution for TYPO3 Core tests [1]. * [1] https://review.typo3.org/c/Packages/TYPO3.CMS/+/87280 Resolves: https://github.com/TYPO3/testing-framework/issues/554 Releases: main --- Classes/Core/Acceptance/Extension/BackendEnvironment.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Classes/Core/Acceptance/Extension/BackendEnvironment.php b/Classes/Core/Acceptance/Extension/BackendEnvironment.php index 1aecb468..758e936e 100644 --- a/Classes/Core/Acceptance/Extension/BackendEnvironment.php +++ b/Classes/Core/Acceptance/Extension/BackendEnvironment.php @@ -281,7 +281,6 @@ public function bootstrapTypo3Environment(SuiteEvent $suiteEvent) $localConfiguration['SYS']['errorHandlerErrors'] = E_ALL; $localConfiguration['SYS']['trustedHostsPattern'] = '.*'; $localConfiguration['SYS']['encryptionKey'] = 'iAmInvalid'; - $localConfiguration['SYS']['features']['redirects.hitCount'] = true; // @todo: This sql_mode should be enabled as soon as styleguide and dataHandler can cope with it //$localConfiguration['SYS']['setDBinit'] = 'SET SESSION sql_mode = \'STRICT_ALL_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_VALUE_ON_ZERO,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,ONLY_FULL_GROUP_BY\';'; $localConfiguration['GFX']['processor'] = 'GraphicsMagick';