Skip to content

Commit 1530ae5

Browse files
committed
[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
1 parent c3d94c4 commit 1530ae5

File tree

5 files changed

+425
-22
lines changed

5 files changed

+425
-22
lines changed

Classes/Composer/ComposerPackageManager.php

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,22 @@
1818
*/
1919

2020
use Composer\InstalledVersions;
21+
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
2122

2223
/**
24+
* `typo3/testing-framework` internal composer package manager, used to gather source
25+
* information of extensions already loaded by the root composer installation with
26+
* the additional ability to register test fixture packages and extensions during
27+
* runtime to create {@see FunctionalTestCase} test instances and provide symlinks
28+
* of extensions into the classic mode test instance or retrieve files from a composer
29+
* package or extension unrelated where they are placed on the filesystem.
30+
*
31+
* - {@see Testbase::setUpInstanceCoreLinks()}
32+
* - {@see Testbase::linkTestExtensionsToInstance()}
33+
* - {@see Testbase::linkFrameworkExtensionsToInstance()}
34+
* - {@see Testbase::setUpLocalConfiguration()}
35+
* - {@see Testbase::setUpPackageStates()}
36+
*
2337
* @internal This class is for testing-framework internal processing and not part of public testing API.
2438
*/
2539
final class ComposerPackageManager
@@ -65,25 +79,29 @@ public function __construct()
6579
$this->build();
6680
}
6781

68-
public function getPackageInfoWithFallback(string $name): ?PackageInfo
82+
/**
83+
* Get composer package information {@see PackageInfo} for `$nameOrExtensionKeyOrPath`.
84+
*/
85+
public function getPackageInfoWithFallback(string $nameOrExtensionKeyOrPath): ?PackageInfo
6986
{
70-
if ($packageInfo = $this->getPackageInfo($name)) {
87+
if ($packageInfo = $this->getPackageInfo($nameOrExtensionKeyOrPath)) {
7188
return $packageInfo;
7289
}
73-
if ($packageInfo = $this->getPackageFromPath($name)) {
90+
if ($packageInfo = $this->getPackageFromPath($nameOrExtensionKeyOrPath)) {
7491
return $packageInfo;
7592
}
76-
if ($packageInfo = $this->getPackageFromPathFallback($name)) {
93+
if ($packageInfo = $this->getPackageFromPathFallback($nameOrExtensionKeyOrPath)) {
7794
return $packageInfo;
7895
}
79-
8096
return null;
8197
}
8298

99+
/**
100+
* Get {@see PackageInfo} for package name or extension key `$name`.
101+
*/
83102
public function getPackageInfo(string $name): ?PackageInfo
84103
{
85-
$name = $this->resolvePackageName($name);
86-
return self::$packages[$name] ?? null;
104+
return self::$packages[$this->resolvePackageName($name)] ?? null;
87105
}
88106

89107
/**
@@ -403,9 +421,23 @@ private function getExtEmConf(string $path): ?array
403421
return null;
404422
}
405423

424+
/**
425+
* Returns resolved composer package name when $name is a known extension key
426+
* for a known package, otherwise return $name unchanged.
427+
*
428+
* Used to determine the package name to look up as composer package within {@see self::$packages}
429+
*
430+
* Supports also relative classic mode notation:
431+
*
432+
* - typo3/sysext/backend
433+
* - typo3conf/ext/my_ext_key
434+
*
435+
* {@see self::prepareResolvePackageName()} for details for normalisation.
436+
*/
406437
private function resolvePackageName(string $name): string
407438
{
408-
return self::$extensionKeyToPackageNameMap[$this->normalizeExtensionKey(basename($name))] ?? $name;
439+
$name = $this->prepareResolvePackageName($name);
440+
return self::$extensionKeyToPackageNameMap[$name] ?? $name;
409441
}
410442

411443
/**
@@ -640,4 +672,47 @@ private function getFirstPathElement(string $path): string
640672
}
641673
return explode('/', $path)[0] ?? '';
642674
}
675+
676+
/**
677+
* Extension can be specified with their composer name, extension key or with classic mode relative path
678+
* prefixes (`typo3/sysext/<extensionkey>` or `typo3conf/ext/<extensionkey>`) for functional tests to
679+
* configure which extension should be provided in the test instance.
680+
*
681+
* This method normalizes a handed over name by removing the specified extra information, so it can be
682+
* used to resolve it either as direct package name or as extension name.
683+
*
684+
* Handed over value also removes known environment prefix paths, like the full path to the root (project rook),
685+
* vendor folder or web folder using {@see self::removePrefixPaths()} which is safe, as this method is and most
686+
* only be used for {@see self::resolvePackageName()} to find a composer package in {@see self::$packages}, after
687+
* mapping extension-key to composer package name.
688+
*
689+
* Example for processed changes:
690+
* -----------------------------_
691+
*
692+
* - typo3/sysext/backend => backend
693+
* - typo3conf/ext/my_ext_key => my_ext_key
694+
*
695+
* Example not processed values:
696+
* -----------------------------
697+
*
698+
* valid names
699+
* - typo3/cms-core => typo3/cms-core
700+
* - my-vendor/my-package-name => my-vendor/my-package-name
701+
* - my-package-name-without-vendor => my-package-name-without-vendor
702+
*/
703+
private function prepareResolvePackageName($name): string
704+
{
705+
$name = trim($this->removePrefixPaths($name), '/');
706+
$relativePrefixPaths = [
707+
'typo3/sysext/',
708+
'typo3conf/ext/',
709+
];
710+
foreach ($relativePrefixPaths as $relativePrefixPath) {
711+
if (!str_starts_with($name, $relativePrefixPath)) {
712+
continue;
713+
}
714+
$name = substr($name, mb_strlen($relativePrefixPath));
715+
}
716+
return $name;
717+
}
643718
}

Classes/Core/Functional/FunctionalTestCase.php

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
108108
*
109109
* A default list of core extensions is always loaded.
110110
*
111+
* System extension can be provided by their extension key or composer package name,
112+
* and also as classic mode relative path
113+
*
114+
* ```
115+
* protected array $coreExensionToLoad = [
116+
* // As composer package name
117+
* 'typo3/cms-core',
118+
* // As extension-key
119+
* 'core',
120+
* // As relative classic mode system installation path
121+
* 'typo3/sysext/core',
122+
* ];
123+
* ```
124+
*
125+
* Note that system extensions must be available, which means either added as require or
126+
* require-dev to the root composer.json or required and installed by a required package.
127+
*
111128
* @see FunctionalTestCaseUtility $defaultActivatedCoreExtensions
112129
*
113130
* @var non-empty-string[]
@@ -118,16 +135,32 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
118135
* Array of test/fixture extensions paths that should be loaded for a test.
119136
*
120137
* This property will stay empty in this abstract, so it is possible
121-
* to just overwrite it in extending classes. Extensions noted here will
122-
* be loaded for every test of a test case, and it is not possible to change
123-
* the list of loaded extensions between single tests of a test case.
138+
* to just overwrite it in extending classes.
139+
*
140+
* IMPORTANT: Extension list is concrete and used to create the test instance on first
141+
* test execution and is **NOT** changeable between single test permutations.
124142
*
125143
* Given path is expected to be relative to your document root, example:
126144
*
127-
* array(
128-
* 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
145+
* ```
146+
* protected array $testExtensionToLoad = [
147+
*
148+
* // Virtual relative classic mode installation path
129149
* 'typo3conf/ext/base_extension',
130-
* );
150+
*
151+
* // Virtual relative classic mode installation path subfolder test fixture
152+
* 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
153+
*
154+
* // Relative to current test case (recommended for test fixture extension)
155+
* __DIR__ . '/../Fixtures/Extensions/another_test_extension',
156+
*
157+
* // composer package name when available as `require` or `require-dev` in root composer.json
158+
* 'vendor/some-extension',
159+
*
160+
* // extension key when available as package loaded as `require` or `require-dev` in root composer.json
161+
* 'my_extension_key',
162+
* ];
163+
* ```
131164
*
132165
* Extensions in this array are linked to the test instance, loaded
133166
* and their ext_tables.sql will be applied.
@@ -144,18 +177,22 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
144177
* be linked for every test of a test case, and it is not possible to change
145178
* the list of folders between single tests of a test case.
146179
*
147-
* array(
180+
* ```
181+
* protected array $pathsToLinkInTestInstance = [
148182
* 'link-source' => 'link-destination'
149-
* );
183+
* ];
184+
* ```
150185
*
151186
* Given paths are expected to be relative to the test instance root.
152187
* The array keys are the source paths and the array values are the destination
153188
* paths, example:
154189
*
155-
* [
190+
* ```
191+
* protected array $pathsToLinkInTestInstance = [
156192
* 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' =>
157193
* 'fileadmin/user_upload',
158-
* ]
194+
* ];
195+
* ```
159196
*
160197
* To be able to link from my_own_ext the extension path needs also to be registered in
161198
* property $testExtensionsToLoad
@@ -169,12 +206,14 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
169206
* paths are really duplicated and provided in the instance - instead of
170207
* using symbolic links. Examples:
171208
*
172-
* [
209+
* ```
210+
* protected array $pathsToProvideInTestInstance = [
173211
* // Copy an entire directory recursive to fileadmin
174212
* 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImages/' => 'fileadmin/',
175213
* // Copy a single file into some deep destination directory
176214
* 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImage/someImage.jpg' => 'fileadmin/_processed_/0/a/someImage.jpg',
177-
* ]
215+
* ];
216+
* ```
178217
*
179218
* @var array<string, non-empty-string>
180219
*/
@@ -205,9 +244,11 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
205244
* To create additional folders add the paths to this array. Given paths are expected to be
206245
* relative to the test instance root and have to begin with a slash. Example:
207246
*
208-
* [
247+
* ```
248+
* protected array $additionalFoldersToCreate = [
209249
* 'fileadmin/user_upload'
210-
* ]
250+
* ];
251+
* ```
211252
*
212253
* @var non-empty-string[]
213254
*/

0 commit comments

Comments
 (0)