Quality assurance
Every tool below is a development dependency of the package and runs from its root directory with
vendor/. Configuration files live in the package root, helper scripts in
Build/. None of them ship with an installed extension.
Overview
| Tool | Configuration | executed on Pull requests | Fails on |
|---|---|---|---|
| Composer | composer. | yes | an invalid or not strictly valid manifest |
| XLIFF schema | xliff- | yes | a language file that violates XLIFF 1.2 strict |
| PHP lint | – | yes | a syntax error on any supported PHP version |
| PHP CS Fixer | .php- | yes | a file the fixer would change |
| Rector | rector. | yes | a file Rector would change |
| PHPStan | phpstan., phpstan- | yes | an error not in the baseline, or a baseline entry that no longer matches |
| Deptrac | deptrac., deptrac. | no | a dependency the layer rules forbid and the baseline does not list |
| PHPUnit | phpunit., phpunit_ | yes, PHP 8.4 and 8.5 | a failing test |
| Line coverage | Build/ | yes | a total below the baseline |
| Infection, gate | infection., Build/ | yes | a changed line below 100 % MSI, uncovered or with a skipped mutant |
| Infection, full run | same, plus Build/ | no | a total MSI or line coverage below the baseline |
A change is ready for review when every tool passes locally. The full mutation run and Deptrac are not part of the pull request pipeline and are the author's to run.
Validation
composer validate
xmllint --schema xliff-core-1.2-strict.xsd --noout $(find Resources -name '*.xlf')
find *.php Classes Configuration Tests -name '*.php' -print0 | xargs -0 -n 1 php -l > /dev/null
PHP CS Fixer
The coding guideline, including the licence header every PHP file carries. The header comes from
the custom fixer in Build/, so a new file gets it by running the fixer, not by
copying it.
vendor/bin/php-cs-fixer fix --dry-run --diff # check
vendor/bin/php-cs-fixer fix # apply
Rector
Keeps the code on current PHP, PHPUnit and TYPO3 idioms: the PHP sets, the prepared sets and the
TYPO3 level set up to v14. Every skip in rector. states why the rule does not apply; a new
skip states its reason too.
vendor/bin/rector process --dry-run # check
vendor/bin/rector process # apply
PHPStan
Runs on level max over Build/, Classes/, Configuration/ and
Tests/. Unmatched ignores are reported, so a fixed error has to leave the baseline too.
The baseline is generated, never edited by hand. Ignores that depend on a dependency's version belong
in phpstan., where regenerating cannot drop them.
vendor/bin/phpstan analyse
vendor/bin/phpstan analyse --generate-baseline
Deptrac
Guards the dependencies between the package's parts. The package provides an import and a representation of the imported data, the latter split into frontend and backend. Each of these is divided into layers, and the rules say which layer may use which.
| Layer | May use |
|---|---|
| ImportCommand | ImportEngine, ImportModel, ImportRepository |
| ImportEngine | ImportModel, ImportRepository, Events |
| ImportIntegration | ImportEngine, ImportModel |
| ImportRepository | ImportModel |
| ImportModel | – |
| FrontendDelivery | FrontendRepository, FrontendModel |
| FrontendRepository | FrontendModel |
| FrontendModel | Events |
| BackendModule | ImportModel, ImportRepository |
| BackendForms | – |
| Service | – |
| System | every layer |
The table lists what a layer may use. In addition, every layer may use Service, while no layer may use System. What these rules ensure:
- A model never uses a repository.
- Import and frontend never use each other.
- The backend sees the import's data, never the import itself.
- Service holds helpers without side effects and the vocabulary several features share. It may depend on nothing in the package, which is what lets every feature use it instead of keeping a copy.
- System holds the upgrade wizards. They change persisted data and are run by the install tool, never by the package's own code.
- Events stands for werkraummedia/events . Only its use from this package is checked.
deptrac. assigns classes to layers by name, so a class moved to another namespace may
change its layer. Every class of the package belongs to exactly one layer. Dependencies on classes
outside the package and
werkraummedia/events
are reported as uncovered, which does not
fail a run.
The baseline lists the violations that existed when the rules were introduced. It only ever shrinks: a fixed violation leaves it, a new one is fixed instead of added.
vendor/bin/deptrac analyse
vendor/bin/deptrac analyse --report-uncovered
vendor/bin/deptrac analyse --formatter=baseline --output=deptrac.baseline.yaml
Writing the baseline exits with an error whenever the previous one missed a violation. Run the analysis again afterwards to check the result.
PHPUnit
phpunit. defines the unit and functional suites. Functional tests run on
SQLite and need the de_ and en_ locales. phpunit_ is the
strict variant: it fails on deprecations, notices and warnings and shows their details.
Paratest runs the suites in parallel, PHPUnit runs a single file more readably.
vendor/bin/paratest --runner=WrapperRunner
vendor/bin/phpunit --testsuite unit
vendor/bin/phpunit -c phpunit_dev.xml.dist Tests/Functional/ImporterTest.php
How import tests are written is described in Testing an import.
Coverage
New and changed code reaches 100 % line coverage. Coverage is always measured over all suites in one run: a per-suite report misses what the other suite covers, and Infection reads this report. It needs a coverage driver such as Xdebug.
XDEBUG_MODE=coverage vendor/bin/paratest --runner=WrapperRunner \
--coverage-xml .coverage/coverage-xml --log-junit .coverage/junit.xml
php Build/Scripts/check-quality-baseline.php
The script compares the total line coverage and the number of uncovered lines with
Build/.
Infection
Mutation testing checks that the tests notice when the code changes. New and changed code reaches
100 % MSI with no skipped mutant. Build/ reads the coverage from
.coverage/, so the coverage run comes first.
- The gate
- Without options, the script mutates only the lines changed against
origin/(main INFECTION_), counts uncovered code as escaped and requires 100 % MSI. Each mutant gets 600 seconds (BASE INFECTION_), and any skipped mutant fails the gate.GATE_ TIMEOUT - The full run
--mutates everything with the timeout fromfull infection.and no threshold. Over the whole package it ends by checking the total MSI and line coverage againstjson5 Build/. A change is not done before a full run has passed.quality- baseline. json
Build/Scripts/infection.sh
Build/Scripts/infection.sh --full
Build/Scripts/infection.sh --full --filter=Classes/Import/Resolver.php
INFECTION_ sets the number of threads, all cores by default.
A skipped mutant means its covering tests outran the timeout. The fix is faster tests, never a longer timeout.
The quality baseline
Build/ records the package's total line coverage, its number of uncovered
lines and its MSI. No change may lower the line coverage or the MSI, or add uncovered lines. No tool
writes the file: when the check reports a value better than the baseline, the change updates it by
hand.