Skip to content

Commit fbe41d2

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 f552cd8 commit fbe41d2

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
@@ -111,6 +111,23 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
111111
*
112112
* A default list of core extensions is always loaded.
113113
*
114+
* System extension can be provided by their extension key or composer package name,
115+
* and also as classic mode relative path
116+
*
117+
* ```
118+
* protected array $coreExensionToLoad = [
119+
* // As composer package name
120+
* 'typo3/cms-core',
121+
* // As extension-key
122+
* 'core',
123+
* // As relative classic mode system installation path
124+
* 'typo3/sysext/core',
125+
* ];
126+
* ```
127+
*
128+
* Note that system extensions must be available, which means either added as require or
129+
* require-dev to the root composer.json or required and installed by a required package.
130+
*
114131
* @see FunctionalTestCaseUtility $defaultActivatedCoreExtensions
115132
*
116133
* @var non-empty-string[]
@@ -121,16 +138,32 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
121138
* Array of test/fixture extensions paths that should be loaded for a test.
122139
*
123140
* This property will stay empty in this abstract, so it is possible
124-
* to just overwrite it in extending classes. Extensions noted here will
125-
* be loaded for every test of a test case, and it is not possible to change
126-
* the list of loaded extensions between single tests of a test case.
141+
* to just overwrite it in extending classes.
142+
*
143+
* IMPORTANT: Extension list is concrete and used to create the test instance on first
144+
* test execution and is **NOT** changeable between single test permutations.
127145
*
128146
* Given path is expected to be relative to your document root, example:
129147
*
130-
* array(
131-
* 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
148+
* ```
149+
* protected array $testExtensionToLoad = [
150+
*
151+
* // Virtual relative classic mode installation path
132152
* 'typo3conf/ext/base_extension',
133-
* );
153+
*
154+
* // Virtual relative classic mode installation path subfolder test fixture
155+
* 'typo3conf/ext/some_extension/Tests/Functional/Fixtures/Extensions/test_extension',
156+
*
157+
* // Relative to current test case (recommended for test fixture extension)
158+
* __DIR__ . '/../Fixtures/Extensions/another_test_extension',
159+
*
160+
* // composer package name when available as `require` or `require-dev` in root composer.json
161+
* 'vendor/some-extension',
162+
*
163+
* // extension key when available as package loaded as `require` or `require-dev` in root composer.json
164+
* 'my_extension_key',
165+
* ];
166+
* ```
134167
*
135168
* Extensions in this array are linked to the test instance, loaded
136169
* and their ext_tables.sql will be applied.
@@ -147,18 +180,22 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
147180
* be linked for every test of a test case, and it is not possible to change
148181
* the list of folders between single tests of a test case.
149182
*
150-
* array(
183+
* ```
184+
* protected array $pathsToLinkInTestInstance = [
151185
* 'link-source' => 'link-destination'
152-
* );
186+
* ];
187+
* ```
153188
*
154189
* Given paths are expected to be relative to the test instance root.
155190
* The array keys are the source paths and the array values are the destination
156191
* paths, example:
157192
*
158-
* [
193+
* ```
194+
* protected array $pathsToLinkInTestInstance = [
159195
* 'typo3/sysext/impext/Tests/Functional/Fixtures/Folders/fileadmin/user_upload' =>
160196
* 'fileadmin/user_upload',
161-
* ]
197+
* ];
198+
* ```
162199
*
163200
* To be able to link from my_own_ext the extension path needs also to be registered in
164201
* property $testExtensionsToLoad
@@ -172,12 +209,14 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
172209
* paths are really duplicated and provided in the instance - instead of
173210
* using symbolic links. Examples:
174211
*
175-
* [
212+
* ```
213+
* protected array $pathsToProvideInTestInstance = [
176214
* // Copy an entire directory recursive to fileadmin
177215
* 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImages/' => 'fileadmin/',
178216
* // Copy a single file into some deep destination directory
179217
* 'typo3/sysext/lowlevel/Tests/Functional/Fixtures/testImage/someImage.jpg' => 'fileadmin/_processed_/0/a/someImage.jpg',
180-
* ]
218+
* ];
219+
* ```
181220
*
182221
* @var array<string, non-empty-string>
183222
*/
@@ -208,9 +247,11 @@ abstract class FunctionalTestCase extends BaseTestCase implements ContainerInter
208247
* To create additional folders add the paths to this array. Given paths are expected to be
209248
* relative to the test instance root and have to begin with a slash. Example:
210249
*
211-
* [
250+
* ```
251+
* protected array $additionalFoldersToCreate = [
212252
* 'fileadmin/user_upload'
213-
* ]
253+
* ];
254+
* ```
214255
*
215256
* @var non-empty-string[]
216257
*/

0 commit comments

Comments
 (0)