[FEATURE] Provision functional test instances in composer mode - #747
Open
bmack wants to merge 1 commit into
Open
[FEATURE] Provision functional test instances in composer mode#747bmack wants to merge 1 commit into
bmack wants to merge 1 commit into
Conversation
TYPO3 ships two installation modes and only one of them was covered by
functional tests: every test instance was a classic mode installation, even
though composer mode is what most projects run. Set
TYPO3_TESTING_INSTANCE_MODE=composer
to provision composer mode instances instead. Classic mode stays the default,
and the mode is part of the instance identifier, so a test case's classic and
composer instances never collide.
A composer mode instance is not its own composer installation. One shared
installation containing every system extension and every fixture extension is
built once per run by Build/Scripts/setupFunctionalComposerSuperset.php, and
each instance borrows its vendor tree and its package artifact by symlink:
<instance>/vendor -> <shared>/vendor
<instance>/public/_assets -> <shared>/public/_assets
<instance>/config/system own configuration
<instance>/public own document root, fileadmin, typo3temp/assets
<instance>/var own caches and logs
Per test case only the *active* package set differs, which is expressed by
narrowing the artifact in memory. Building a real installation per test
configuration was measured at about five seconds each, which for the roughly
two hundred configurations in this repository would cost far more than the
provisioning this saves.
Classes are deliberately not loaded from the shared installation. The root
autoloader already maps every system extension and every fixture extension, so
no second autoloader is registered and no second copy of any third party
package is ever loaded. One consequence: class alias maps that the shared
installation registered at install time are not active, because the root
installation never processed those packages.
Notable details, each of which was a bug on the way and none of which fails
loudly:
* TYPO3 decides between the classic layout (typo3conf, typo3temp/var) and the
composer layout (config, var) by whether the project root and the document
root differ - not by composer mode. Test instances therefore set
TYPO3_PATH_ROOT to the public directory in composer mode.
* Package::getPackagePath() resolves the artifact's relative path against
Environment::getProjectPath() once and writes the result back. Caching one
PackageCacheEntry for the process therefore binds every package to whichever
instance touched it first, and every later instance looks for its packages
inside that instance. Only the raw artifact data is cached; the entry is
rebuilt whenever the instance changes.
* On the command line in composer mode TYPO3 reports the console binary as the
entry script, because in a real installation it sits outside the document
root. Functional tests are command line processes simulating web requests,
and NormalizedParams derives the site URL by subtracting the entry script's
directory from the request - so it subtracted typo3/sysext/core/bin/ from
every request, the site path collapsed to an empty string, and every
generated link silently lost its leading slash. The front end entry script
is reported instead, which is what a web request has.
* A console command run in a sub process cannot use EXT:core/bin/typo3: that
script derives its autoloader from its own directory, and since the shared
installation's packages are symlinks into the checkout, it resolves back to
the root autoloader, never defines TYPO3_COMPOSER_MODE and runs in classic
mode. Composer mode instances get a generated entry point instead, which
getCliEntryPoint() hands to the test.
* Applications bootstrapped by a test - the install tool, notably - must boot
through bootTestInstance(), because Bootstrap resolves the artifact from the
root installation, and the mono repo has none.
* fileadmin and typo3temp/assets are web accessible and belong below public/.
Getting that wrong makes the default file storage point at nothing.
* Frontend sub requests bootstrap a second time and need the same narrowed
package set. Their server parameters also have to name the real document
root, or the site path is derived from a script outside the public path.
* The sites configuration cache removal in tearDown() has to use the layout's
variable path, or it silently does nothing and the stale configuration leaks
into the next test.
* Paths declared by test cases are given relative to the document root, which
only carries the extension tree in classic mode; they now fall back to the
original root.
A "PKG:typo3/app:" resource identifier carries a path relative to the project
root, which is the document root only in classic mode, so
getAppRelativePublicPath() is provided for tests naming such resources.
TestingBootstrapRunner overrides Bootstrap::createPackageCache() through late
static binding, so no change to typo3/cms is required.
bmack
force-pushed
the
composer-mode-standalone
branch
from
September 8, 2026 10:49
465ac8e to
b1cefe1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds composer mode to functional test instances. Today every instance is a classic
mode installation, even though composer mode is what nearly every project runs, so a
whole installation layout goes untested.
Classic mode stays the default. The mode is part of the instance identifier, so a test
case's classic and composer instances never collide.
This replaces the stacked version
An earlier form of this work sat on top of #740 and needed its configuration-keyed
instance sharing. It does not any more. This branch applies to
mainon its own,keeps
getInstanceIdentifier()/getInstancePath()static exactly as they are today,and introduces no breaking change — it is a plain
[FEATURE].That reordering is backed by a measurement rather than a preference. One chunk of the
TYPO3 core functional corpus (2117 tests), four arms, same machine:
Composer mode without any instance sharing is faster than classic mode with it.
Sharing is worth a further ~16 %; composer mode itself is worth 35 %. So the sharing
mechanism is an optimisation on top, not a prerequisite, and it should be reviewed on
its own merits instead of blocking this.
How a composer mode instance is built
An instance is not its own composer installation. One shared installation containing
every system extension and every fixture extension is built once per run, and each
instance borrows its vendor tree and package artifact by symlink:
Per test case only the active package set differs, expressed by narrowing the artifact
in memory. A real installation per test configuration measures ~3.4 s, which across the
~142 distinct extension sets in core would cost more than this saves — though that
trade-off is worth revisiting, see below.
Classes are deliberately not loaded from the shared installation. The root
autoloader already maps every system extension and every fixture extension, so no second
autoloader is registered and no second copy of any third-party package is loaded.
What to look at
Most of the diff is mechanism. Two things deserve real scrutiny:
ComposerModeInstance::createPackageCache()— the narrowing. It hands the packagemanager every installed package while marking only some active. In a real composer
installation installed and active are always the same set; there is no
installed-but-inactive state. This is the one place the model departs from reality,
and the direction of travel is to replace it with one real installation per extension
set (measured: ~3.4 s each, ~1.8 MB of distinct bytes per set, the third-party tree
being byte-identical across all of them).
SystemEnvironmentBuilder::determineCurrentScript()— on the CLI, composer modereports the console binary as the entry script, because in a real installation it sits
outside the document root. Functional tests are CLI processes simulating web requests,
and
NormalizedParamsderives the site URL by subtracting the entry script's directoryfrom the request. Left alone it subtracted
typo3/sysext/core/bin/, the site pathcollapsed to an empty string, and every generated link silently lost its leading slash.
Everything else in the commit message is a bug that was hit on the way, each of which
failed far from its cause.
Verification
Both modes on the same corpus chunk, run back to back:
The full corpus has been run in both modes against all four DBMS with matching test
counts. CGL, lint, PHPStan and the unit suite are green locally.
Core needs a companion patch for tests that assume the classic layout; it is prepared and
waits on this.