From 92480928c6526c7c7a0f4037ddfafee78d978d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Buchmann?= Date: Sun, 29 Mar 2026 23:11:39 +0200 Subject: [PATCH 1/3] [TASK] Apply recommended testing structure Inspired by the tea extension --- .editorconfig | 55 +- .gitattributes | 10 +- .github/ISSUE_TEMPLATE/bug.md | 32 + .github/ISSUE_TEMPLATE/feature.md | 21 + .github/ISSUE_TEMPLATE/task.md | 21 + .github/workflows/ci.yml | 228 ++++++ .github/workflows/codecoverage.yml | 95 +++ .github/workflows/publish.yml | 20 + .github/workflows/release.yaml | 21 - .github/workflows/test.yaml | 101 --- .gitignore | 9 + .php-cs-fixer.php | 272 ------- .phpstan.neon | 8 - Build/Scripts/runTests.sh | 755 ++++++++++++++++++ Build/composer-unused/composer-unused.php | 8 + Build/php-cs-fixer/config.php | 21 + Build/phpstan/phpstan-baseline.neon | 2 + Build/phpstan/phpstan.neon | 64 ++ Build/phpunit/FunctionalTests.xml | 24 + Build/phpunit/UnitTests.xml | 17 + Build/rector/config.php | 65 ++ CODE_OF_CONDUCT.md | 7 + Classes/Command/CrawlSitemapCommand.php | 41 +- Classes/Exception/Exception.php | 4 +- Classes/Exception/InvalidFormatException.php | 4 +- Classes/Exception/InvalidHeadersException.php | 4 +- Classes/Exception/InvalidUrlException.php | 4 +- Classes/Helper/Error.php | 4 +- .../Command/CrawlSitemapCommandTest.php | 2 + Tests/FunctionalTests.xml | 15 +- Tests/Unit/Environment/ExtensionTest.php | 25 + composer.json | 160 +++- ext_emconf.php | 1 - phive.xml | 4 + 34 files changed, 1612 insertions(+), 512 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/feature.md create mode 100644 .github/ISSUE_TEMPLATE/task.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/codecoverage.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .github/workflows/release.yaml delete mode 100644 .github/workflows/test.yaml delete mode 100644 .php-cs-fixer.php delete mode 100644 .phpstan.neon create mode 100755 Build/Scripts/runTests.sh create mode 100644 Build/composer-unused/composer-unused.php create mode 100644 Build/php-cs-fixer/config.php create mode 100644 Build/phpstan/phpstan-baseline.neon create mode 100644 Build/phpstan/phpstan.neon create mode 100644 Build/phpunit/FunctionalTests.xml create mode 100644 Build/phpunit/UnitTests.xml create mode 100644 Build/rector/config.php create mode 100644 CODE_OF_CONDUCT.md create mode 100644 Tests/Unit/Environment/ExtensionTest.php create mode 100644 phive.xml diff --git a/.editorconfig b/.editorconfig index f2fd613..787ba5a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,5 @@ -# EditorConfig is awesome: http://EditorConfig.org +# top-most EditorConfig file +root = true # Unix-style newlines with a newline ending every file [*] @@ -9,46 +10,44 @@ indent_size = 4 insert_final_newline = true trim_trailing_whitespace = true -[*.{ts,js}] -indent_style = space +# JS files +[*.js] indent_size = 2 -# JSON-Files +# JSON files [*.json] indent_style = tab -indent_size = 4 - -# ReST-Files -[*.rst] -indent_style = space -indent_size = 3 # package.json -# .travis.yml -# bower.json -[{package.json,.travis.yml,bower.json,composer.json}] -indent_style = space +[package.json] indent_size = 2 -# TypoScript -[{*.typoscript,*.tsconfig,setup.txt,constants.txt,ext_typoscript_setup.txt,ext_typoscript_constants.txt,ext_conf_template.txt}] -indent_style = space +# ReST files +[{*.rst,*.rst.txt}] +indent_size = 4 +max_line_length = 80 + +# SQL files +[*.sql] +indent_style = tab indent_size = 2 -# XML-Files -[{*.xml,*.xlf}] -indent_style = space +# TypoScript files +[*.{typoscript,tsconfig}] indent_size = 2 -# SQL-Files -[*.sql] -indent_style = tab +# YAML files +[{*.yml,*.yaml}] +indent_size = 2 + +# XLF files +[*.xlf] indent_size = 2 -# ChangeLog files -[{ChangeLog,ChangeLog.txt}] +# .htaccess +[.htaccess] indent_style = tab -# NEON-Files -[*.neon] -indent_size = 2 +# Markdown files +[*.md] +max_line_length = 80 diff --git a/.gitattributes b/.gitattributes index c48541d..29d701a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,13 +1,9 @@ /.Build/ export-ignore +/Build/ export-ignore /Tests/ export-ignore /.editorconfig export-ignore /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore -/.gitlab-ci.yml export-ignore -/.scrutinizer export-ignore -/.styleci.yml export-ignore -/.travis.yml export-ignore -/.php_cs.dist export-ignore -/.php-cs-fixer.php export-ignore -/,phpstan.neon export-ignore +/phive.xml export-ignore +/tools/ export-ignore binary diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md new file mode 100644 index 0000000..a0dfc9c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -0,0 +1,32 @@ +--- +name: Bug +about: Something is broken. +title: '' +type: bug +assignees: '' + +--- + +## Summary + +(one or two sentences summarizing the problem) + +## Steps to reproduce the problem + +1. … +2. … +3. … +4. … + +## Actual behavior + +(A description of the faulty behavior you're seeing. Add error messages or +screenshot if possible/helpful.) + +## Expected behaviour + +(what you expect to happen instead) + +## Additional information + +(additional information that might help understand and fix this bug) diff --git a/.github/ISSUE_TEMPLATE/feature.md b/.github/ISSUE_TEMPLATE/feature.md new file mode 100644 index 0000000..41453e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.md @@ -0,0 +1,21 @@ +--- +name: Feature +about: We would like to have something new. +title: '' +type: feature +assignees: '' + +--- + +## Story + +As …, I want …, because …. + +## Acceptance criteria + +- [ ] … +- [ ] … + +## Additional information + +(additional information that might help understand and implement this feature) diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..d2544a5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.md @@ -0,0 +1,21 @@ +--- +name: Task +about: Something is to be done, and it is neither a bug nor a new feature. +title: '' +type: task +assignees: '' + +--- + +## Story + +As …, I want …, because …. + +## Acceptance criteria + +- [ ] … +- [ ] … + +## Additional information + +(additional information that might help understand and implement this task) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5a4cca1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,228 @@ +--- +# This GitHub Actions workflow uses the same development tools that are also installed locally +# via Composer or PHIVE and calls them using the Composer scripts. +name: GitHub Actions CI +on: + push: + branches: + - main + pull_request: + schedule: + - cron: '15 3 * * 1' +permissions: + contents: read +jobs: + php-lint: + name: PHP linter + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install Composer dependencies + run: Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -s composerUpdateMax -t 13.4 + - name: Lint PHP + run: | + Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -s lintPhp + strategy: + fail-fast: false + matrix: + php-version: + - "8.1" + - "8.2" + - "8.3" + - "8.4" + - "8.5" + code-quality: + name: Code quality checks + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Show the Composer version + run: ./Build/Scripts/runTests.sh -s composer -- --version + - name: Show the Composer configuration + run: ./Build/Scripts/runTests.sh -s composer config --global --list + - name: Install Composer dependencies + run: Build/Scripts/runTests.sh -s composerUpdateMax + - name: Run code quality checks + run: | + Build/Scripts/runTests.sh -s composer check:${{ matrix.command }} + strategy: + fail-fast: false + matrix: + command: + - "composer:psr-verify" + - "composer:unused" + - "php:cs-fixer" + - "php:rector" + - "php:stan" + - "yaml:lint" + static-code-analysis: + name: Static code quality checks + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Show the Composer version + run: ./Build/Scripts/runTests.sh -s composer -- --version + - name: Show the Composer configuration + run: ./Build/Scripts/runTests.sh -s composer config --global --list + - name: Install Composer dependencies + run: Build/Scripts/runTests.sh -s composerUpdateMax + - name: Run code quality checks + run: | + Build/Scripts/runTests.sh -s ${{ matrix.command }} + strategy: + fail-fast: false + matrix: + command: + - "checkComposerNormalize" + check-fixers: + name: Check fixers + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Show the Composer version + run: ./Build/Scripts/runTests.sh -s composer -- --version + - name: Show the Composer configuration + run: ./Build/Scripts/runTests.sh -s composer config --global --list + - name: Install Composer dependencies + run: Build/Scripts/runTests.sh -s composerUpdateMax + - name: Runs all automatic code style fixes. + run: Build/Scripts/runTests.sh -s fix + prepare-release: + name: Check prepare release script + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Run prepare release + run: Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -s composer prepare-release + strategy: + fail-fast: false + matrix: + php-version: + - "8.4" + unit-tests: + name: Unit tests + runs-on: ubuntu-24.04 + needs: php-lint + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Show the Composer version + run: ./Build/Scripts/runTests.sh -s composer -- --version + - name: Show the Composer configuration + run: ./Build/Scripts/runTests.sh -s composer config --global --list + - name: Install composer dependencies + run: | + ./Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -t ${{matrix.typo3-version}} -s composerUpdate${{matrix.composer-dependencies}} + - name: Run unit tests + run: | + ./Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -s unit + strategy: + fail-fast: false + matrix: + include: + - typo3-version: "12.4" + php-version: "8.1" + composer-dependencies: Min + - typo3-version: "12.4" + php-version: "8.1" + composer-dependencies: Max + - typo3-version: "12.4" + php-version: "8.5" + composer-dependencies: Min + - typo3-version: "12.4" + php-version: "8.5" + composer-dependencies: Max + - typo3-version: "13.4" + php-version: "8.2" + composer-dependencies: Min + - typo3-version: "13.4" + php-version: "8.5" + composer-dependencies: Max + - typo3-version: "14.1" + php-version: "8.4" + composer-dependencies: Min + - typo3-version: "14.1" + php-version: "8.5" + composer-dependencies: Max + functional-tests: + name: Functional tests + runs-on: ubuntu-24.04 + needs: php-lint + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Show the Composer version + run: ./Build/Scripts/runTests.sh -s composer -- --version + - name: Show the Composer configuration + run: ./Build/Scripts/runTests.sh -s composer config --global --list + - name: Install composer dependencies + run: | + ./Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -t ${{matrix.typo3-version}} -s composerUpdate${{matrix.composer-dependencies}} + - name: Run functional tests + run: | + ./Build/Scripts/runTests.sh -p ${{ matrix.php-version }} -s functional + strategy: + fail-fast: false + matrix: + include: + - typo3-version: "12.4" + php-version: "8.1" + composer-dependencies: Min + - typo3-version: "12.4" + php-version: "8.1" + composer-dependencies: Max + - typo3-version: "12.4" + php-version: "8.5" + composer-dependencies: Min + - typo3-version: "12.4" + php-version: "8.5" + composer-dependencies: Max + - typo3-version: "13.4" + php-version: "8.2" + composer-dependencies: Min + - typo3-version: "13.4" + php-version: "8.2" + composer-dependencies: Max + - typo3-version: "13.4" + php-version: "8.5" + composer-dependencies: Min + - typo3-version: "13.4" + php-version: "8.5" + composer-dependencies: Max + - typo3-version: "14.1" + php-version: "8.4" + composer-dependencies: Min + - typo3-version: "14.1" + php-version: "8.4" + composer-dependencies: Max + - typo3-version: "14.1" + php-version: "8.5" + composer-dependencies: Min + - typo3-version: "14.1" + php-version: "8.5" + composer-dependencies: Max + shellcheck: + name: Check shell scripts + runs-on: ubuntu-24.04 + timeout-minutes: 2 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Check scripts + run: | + Build/Scripts/runTests.sh -s shellcheck +# documentation: +# name: Documentation +# runs-on: ubuntu-24.04 +# timeout-minutes: 2 +# steps: +# - name: Checkout +# uses: actions/checkout@v6 +# - name: Test if the documentation will render without warnings +# run: | +# Build/Scripts/runTests.sh -s docsGenerate diff --git a/.github/workflows/codecoverage.yml b/.github/workflows/codecoverage.yml new file mode 100644 index 0000000..48141ff --- /dev/null +++ b/.github/workflows/codecoverage.yml @@ -0,0 +1,95 @@ +--- +# This GitHub Actions workflow calculates the code coverage of the extension and uploads it to coveralls.io. +name: Code coverage +on: + push: + branches: + - main + pull_request: +permissions: + contents: read +jobs: + code-coverage: + name: Calculate code coverage + permissions: + actions: write + runs-on: ubuntu-24.04 + env: + DB_DATABASE: typo3 + DB_USER: root + DB_PASSWORD: root + DB_HOST: localhost + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "${{ matrix.php-version }}" + ini-file: development + tools: composer:v2, phive + extensions: mysqli + coverage: xdebug + - name: Install development tools + run: phive --no-progress install --trust-gpg-keys D8406D0D82947747293778314AA394086372C20A + - name: Show Composer version + run: composer --version + - name: Show the Composer configuration + run: composer config --global --list + - name: Cache dependencies installed with composer + uses: actions/cache@v5 + with: + key: "php-${{ matrix.php-version }}-typo3-${{ matrix.typo3-version }}-${{ matrix.composer-dependencies }}-composer-${{ hashFiles('**/composer.json') }}" + path: ~/.cache/composer + restore-keys: "php-${{ matrix.php-version }}-typo3-${{ matrix.typo3-version }}-${{ matrix.composer-dependencies }}-composer-\n" + - name: Install TYPO3 Core + env: + TYPO3: "${{ matrix.typo3-version }}" + run: | + composer require --no-ansi --no-interaction --no-progress --no-install typo3/cms-core:^"$TYPO3" + composer show + - name: Install lowest dependencies with composer + if: "matrix.composer-dependencies == 'Min'" + run: | + composer update --no-ansi --no-interaction --no-progress --with-dependencies --prefer-lowest + composer show + - name: Install highest dependencies with composer + if: "matrix.composer-dependencies == 'Max'" + run: | + composer update --no-ansi --no-interaction --no-progress --with-dependencies + composer show + - name: Start MySQL + run: "sudo /etc/init.d/mysql start" + - name: Run unit tests with coverage + run: composer check:coverage:unit + - name: Show generated coverage files + run: "ls -lahR build/coverage/" + - name: Run functional tests with coverage + run: | + export typo3DatabaseName="$DB_DATABASE"; + export typo3DatabaseHost="$DB_HOST"; + export typo3DatabaseUsername="$DB_USER"; + export typo3DatabasePassword="$DB_PASSWORD"; + composer check:coverage:functional + - name: Show generated coverage files + run: "ls -lahR build/coverage/" + - name: Merge coverage results + run: composer check:coverage:merge + - name: Show combined coverage files + run: "ls -lahR build/logs/" + - name: Upload coverage results to Coveralls + uses: coverallsapp/github-action@v2 + with: + fail-on-error: false + env: + github-token: ${{ secrets.GITHUB_TOKEN }} + # Note: This is the only path that the Coveralls GitHub Action supports. + # So we cannot use something like .Build/coverage/clover.xml here. + file: build/logs/clover.xml + strategy: + fail-fast: false + matrix: + include: + - typo3-version: "13.4" + php-version: "8.4" + composer-dependencies: Max diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..36ebfb3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,20 @@ +name: Publish + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' +permissions: + contents: read +jobs: + publish: + permissions: + actions: write + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Publish new version to TER + uses: tomasnorre/typo3-upload-ter@v2 + with: + api-token: ${{ secrets.TYPO3_API_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 5d4ea32..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "TER deployment" - -on: - push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' - -jobs: - # The actual TER upload job - ter-release: - name: "TYPO3 TER release" - - runs-on: ubuntu-latest - steps: - - name: "Checkout" - uses: actions/checkout@v3 - - name: "Publish new version to TER" - uses: tomasnorre/typo3-upload-ter@v2 - with: - api-token: ${{ secrets.TYPO3_API_TOKEN }} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index e4204a2..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,101 +0,0 @@ ---- -name: Test - -on: [push, pull_request] - -jobs: - php-lint: - name: "PHP linter" - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: [8.1, 8.2, 8.3, 8.4, 8.5] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: "Install PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php-versions }}" - - name: "Cache composer dependencies" - uses: actions/cache@v4 - with: - path: ~/.composer/cache - key: php-${{ matrix.php-versions }} - -composer-${{ hashFiles('composer.json') }} - restore-keys: | - php-${{ matrix.php-versions }}-composer- - - name: "Install composer dependencies" - run: composer install --no-progress - - name: "Run PHP lint" - run: "composer run-script test-php-lint" - php-cs-fixer: - name: "PHP-CS-Fixer" - runs-on: ubuntu-latest - strategy: - matrix: - php-versions: [8.3] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: "Install PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php-versions }}" - - name: "Cache composer dependencies" - uses: actions/cache@v4 - with: - path: ~/.composer/cache - key: php-${{ matrix.php-versions }} - -composer-${{ hashFiles('composer.json') }} - restore-keys: | - php-${{ matrix.php-versions }}-composer- - - name: "Install composer dependencies" - run: composer install --no-progress - - name: "Run PHP-CS-Fixer" - run: "composer run-script test-php-cs-fixer" - tests: - runs-on: ubuntu-latest - strategy: - max-parallel: 2 - matrix: - variants: - - {typo3: 14, php: 8.5} - - {typo3: 14, php: 8.4} - - {typo3: 13, php: 8.3} - - {typo3: 13, php: 8.2} - - {typo3: 12, php: 8.4} - - {typo3: 12, php: 8.1} - - name: "Run tests with PHP ${{ matrix.variants.php }} - using TYPO3 ${{ matrix.variants.typo3 }}" - steps: - - uses: actions/checkout@v4 - - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.variants.php }} - extensions: xml, mbstring, curl - - - name: "Cache composer dependencies" - uses: actions/cache@v4 - with: - path: ~/.composer/cache - key: php-${{ matrix.variants.php }} - -typo3-${{ matrix.variants.typo3 }} - -composer-${{ hashFiles('composer.json') }} - restore-keys: | - php-${{ matrix.variants.php }} - -typo3-${{ matrix.variants.typo3 }} - -composer-${{ hashFiles('composer.json') }} - - - name: "Install composer dependencies" - run: composer require typo3/minimal - "^${{ matrix.variants.typo3 }}" - --prefer-dist --no-progress --no-suggest - - - name: "Run PHPStan tests" - run: composer run-script test-phpstan - - - name: "Run Functional tests" - run: composer run-script test-functional diff --git a/.gitignore b/.gitignore index 1113a69..651c17a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,12 @@ composer.lock **/.phpunit.result.cache /Tests/.phpunit.cache/ +/*.idea +/.cache +/.php-cs-fixer.cache +/.phpunit.result.cache +/Documentation-GENERATED-temp/ +/build +/generate-documentation.sh +/nbproject +/var diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php deleted file mode 100644 index 8d4b3ad..0000000 --- a/.php-cs-fixer.php +++ /dev/null @@ -1,272 +0,0 @@ -setRules([ - // PHP arrays should be declared using the configured syntax. - 'array_syntax' => true, - // Converts backtick operators to `shell_exec` calls. - 'backtick_to_shell_exec' => true, - // Binary operators should be surrounded by space as configured. - 'binary_operator_spaces' => true, - // There MUST be one blank line after the namespace declaration. - 'blank_line_after_namespace' => true, - // Ensure there is no code on the same line as the PHP open tag and it is followed by a blank line. - 'blank_line_after_opening_tag' => true, - // An empty line feed must precede any configured statement. - 'blank_line_before_statement' => ['statements'=>['return']], - 'single_space_around_construct' => true, - 'control_structure_braces' => true, - 'control_structure_continuation_position' => true, - 'declare_parentheses' => true, - 'no_multiple_statements_per_line' => true, - 'braces_position' => true, - 'statement_indentation' => true, - // A single space or none should be between cast and variable. - 'cast_spaces' => false, - // Class, trait and interface elements must be separated with one or none blank line. - 'class_attributes_separation' => ['elements'=>['method'=>'one', 'trait_import' => 'none']], - // Whitespace around the keywords of a class, trait or interfaces definition should be one space. - 'class_definition' => ['single_line'=>true], - // Namespace must not contain spacing, comments or PHPDoc. - 'clean_namespace' => true, - // Remove extra spaces in a nullable typehint. - 'compact_nullable_type_declaration' => true, - // Concatenation should be spaced according configuration. - 'concat_space' => false, - // The PHP constants `true`, `false`, and `null` MUST be written using the correct casing. - 'constant_case' => true, - // Equal sign in declare statement should be surrounded by spaces or not following configuration. - 'declare_equal_normalize' => true, - // Doctrine annotations must use configured operator for assignment in arrays. - 'doctrine_annotation_array_assignment' => ['operator'=>':'], - // Doctrine annotations without arguments must use the configured syntax. - 'doctrine_annotation_braces' => true, - // Doctrine annotations must be indented with four spaces. - 'doctrine_annotation_indentation' => true, - // Fixes spaces in Doctrine annotations. - 'doctrine_annotation_spaces' => ['before_array_assignments_colon'=>false], - // Replaces short-echo ` true, - // The keyword `elseif` should be used instead of `else if` so that all control keywords look like single words. - 'elseif' => true, - // PHP code MUST use only UTF-8 without BOM (remove BOM). - 'encoding' => true, - // PHP code must use the long ` true, - // Transforms imported FQCN parameters and return types in function arguments to short version. - 'fully_qualified_strict_types' => true, - // Spaces should be properly placed in a function declaration. - 'function_declaration' => true, - // Ensure single space between function's argument and its typehint. - 'type_declaration_spaces' => true, - // Renames PHPDoc tags. - 'general_phpdoc_tag_rename' => ['replacements'=>['inheritDocs'=>'inheritDoc']], - // Include/Require and file path should be divided with a single space. File path should not be placed under brackets. - 'include' => true, - // Pre- or post-increment and decrement operators should be used if possible. - 'increment_style' => true, - // Code MUST use configured indentation type. - 'indentation_type' => true, - // Lambda must not import variables it doesn't use. - 'lambda_not_used_import' => true, - // All PHP files must use same line ending. - 'line_ending' => true, - // Ensure there is no code on the same line as the PHP open tag. - 'linebreak_after_opening_tag' => true, - // Cast should be written in lower case. - 'lowercase_cast' => true, - // PHP keywords MUST be in lower case. - 'lowercase_keywords' => true, - // Class static references `self`, `static` and `parent` MUST be in lower case. - 'lowercase_static_reference' => true, - // Magic constants should be referred to using the correct casing. - 'magic_constant_casing' => true, - // Magic method definitions and calls must be using the correct casing. - 'magic_method_casing' => true, - // In method arguments and method call, there MUST NOT be a space before each comma and there MUST be one space after each comma. Argument lists MAY be split across multiple lines, where each subsequent line is indented once. When doing so, the first item in the list MUST be on the next line, and there MUST be only one argument per line. - 'method_argument_space' => ['on_multiline'=>'ignore'], - // Function defined by PHP should be called using the correct casing. - 'native_function_casing' => true, - // Native type hints for functions should use the correct case. - 'native_type_declaration_casing' => true, - // All instances created with new keyword must be followed by braces. - 'new_with_parentheses' => true, - // Master language constructs shall be used instead of aliases. - 'no_alias_language_construct_call' => true, - // Replace control structure alternative syntax to use braces. - 'no_alternative_syntax' => true, - // There should not be a binary flag before strings. - 'no_binary_string' => true, - // There should be no empty lines after class opening brace. - 'no_blank_lines_after_class_opening' => true, - // There should not be blank lines between docblock and the documented element. - 'no_blank_lines_after_phpdoc' => true, - // There must be a comment when fall-through is intentional in a non-empty case body. - 'no_break_comment' => true, - // The closing `? >` tag MUST be omitted from files containing only PHP. - 'no_closing_tag' => true, - // There should not be any empty comments. - 'no_empty_comment' => true, - // There should not be empty PHPDoc blocks. - 'no_empty_phpdoc' => true, - // Remove useless (semicolon) statements. - 'no_empty_statement' => true, - // Removes extra blank lines and/or blank lines following configuration. - 'no_extra_blank_lines' => ['tokens'=>['case','continue','curly_brace_block','default','extra','parenthesis_brace_block','square_brace_block','switch','throw','use']], - // Remove leading slashes in `use` clauses. - 'no_leading_import_slash' => true, - // The namespace declaration line shouldn't contain leading whitespace. - 'no_leading_namespace_whitespace' => true, - // Either language construct `print` or `echo` should be used. - 'no_mixed_echo_print' => true, - // Operator `=>` should not be surrounded by multi-line whitespaces. - 'no_multiline_whitespace_around_double_arrow' => true, - // Short cast `bool` using double exclamation mark should not be used. - 'no_short_bool_cast' => true, - // Single-line whitespace before closing semicolon are prohibited. - 'no_singleline_whitespace_before_semicolons' => true, - // When making a method or function call, there MUST NOT be a space between the method or function name and the opening parenthesis. - 'no_spaces_after_function_name' => true, - // There MUST NOT be spaces around offset braces. - 'no_spaces_around_offset' => true, - // There MUST NOT be a space after the opening parenthesis. There MUST NOT be a space before the closing parenthesis. - 'spaces_inside_parentheses' => true, - // Removes `@param`, `@return` and `@var` tags that don't provide any useful information. - 'no_superfluous_phpdoc_tags' => ['allow_mixed'=>true,'allow_unused_params'=>true], - // Remove trailing commas in list function calls. - // PHP single-line arrays should not have trailing comma. - 'no_trailing_comma_in_singleline' => true, - // Remove trailing whitespace at the end of non-blank lines. - 'no_trailing_whitespace' => true, - // There MUST be no trailing spaces inside comment or PHPDoc. - 'no_trailing_whitespace_in_comment' => true, - // Removes unneeded parentheses around control statements. - 'no_unneeded_control_parentheses' => ['statements'=>['break','clone','continue','echo_print','return','switch_case','yield','yield_from']], - // Removes unneeded curly braces that are superfluous and aren't part of a control structure's body. - 'no_unneeded_braces' => true, - // Variables must be set `null` instead of using `(unset)` casting. - 'no_unset_cast' => true, - // Unused `use` statements must be removed. - 'no_unused_imports' => true, - // In array declaration, there MUST NOT be a whitespace before each comma. - 'no_whitespace_before_comma_in_array' => true, - // Remove trailing whitespace at the end of blank lines. - 'no_whitespace_in_blank_line' => true, - // Array index should always be written by using square braces. - 'normalize_index_brace' => true, - // There should not be space before or after object operators `->` and `?->`. - 'object_operator_without_whitespace' => true, - // Orders the elements of classes/interfaces/traits. - 'ordered_class_elements' => ['order'=>['use_trait']], - // Ordering `use` statements. - 'ordered_imports' => true, - // PHPUnit annotations should be a FQCNs including a root namespace. - 'php_unit_fqcn_annotation' => true, - // Enforce camel (or snake) case for PHPUnit test methods, following configuration. - 'php_unit_method_casing' => true, - // All items of the given phpdoc tags must be either left-aligned or (by default) aligned vertically. - 'phpdoc_align' => false, - // PHPDoc annotation descriptions should not be a sentence. - 'phpdoc_annotation_without_dot' => true, - // Docblocks should have the same indentation as the documented subject. - 'phpdoc_indent' => true, - // Fixes PHPDoc inline tags. - 'phpdoc_inline_tag_normalizer' => true, - // `@access` annotations should be omitted from PHPDoc. - 'phpdoc_no_access' => true, - // No alias PHPDoc tags should be used. - 'phpdoc_no_alias_tag' => true, - // `@package` and `@subpackage` annotations should be omitted from PHPDoc. - 'phpdoc_no_package' => true, - // Classy that does not inherit must not have `@inheritdoc` tags. - 'phpdoc_no_useless_inheritdoc' => true, - // The type of `@return` annotations of methods returning a reference to itself must the configured one. - 'phpdoc_return_self_reference' => true, - // Scalar types should always be written in the same form. `int` not `integer`, `bool` not `boolean`, `float` not `real` or `double`. - 'phpdoc_scalar' => true, - // Annotations in PHPDoc should be grouped together so that annotations of the same type immediately follow each other, and annotations of a different type are separated by a single blank line. - 'phpdoc_separation' => true, - // Single line `@var` PHPDoc should have proper spacing. - 'phpdoc_single_line_var_spacing' => true, - // PHPDoc summary should end in either a full stop, exclamation mark, or question mark. - 'phpdoc_summary' => false, - // Forces PHPDoc tags to be either regular annotations or inline. - 'phpdoc_tag_type' => ['tags'=>['inheritDoc'=>'inline']], - // Docblocks should only be used on structural elements. - 'phpdoc_to_comment' => true, - // PHPDoc should start and end with content, excluding the very first and last line of the docblocks. - 'phpdoc_trim' => true, - // Removes extra blank lines after summary and after description in PHPDoc. - 'phpdoc_trim_consecutive_blank_line_separation' => true, - // The correct case must be used for standard PHP types in PHPDoc. - 'phpdoc_types' => true, - // Sorts PHPDoc types. - 'phpdoc_types_order' => ['null_adjustment'=>'always_last','sort_algorithm'=>'none'], - // `@var` and `@type` annotations of classy properties should not contain the name. - 'phpdoc_var_without_name' => true, - // Converts `protected` variables and methods to `private` where possible. - 'protected_to_private' => true, - // There should be one or no space before colon, and one space after it in return type declarations, according to configuration. - 'return_type_declaration' => true, - // Instructions must be terminated with a semicolon. - 'semicolon_after_instruction' => true, - // Cast `(boolean)` and `(integer)` should be written as `(bool)` and `(int)`, `(double)` and `(real)` as `(float)`, `(binary)` as `(string)`. - 'short_scalar_cast' => true, - // A PHP file without end tag must always end with a single empty line feed. - 'single_blank_line_at_eof' => true, - // There should be exactly one blank line before a namespace declaration. - 'blank_lines_before_namespace' => true, - // There MUST NOT be more than one property or constant declared per statement. - 'single_class_element_per_statement' => true, - // There MUST be one use keyword per declaration. - 'single_import_per_statement' => true, - // Each namespace use MUST go on its own line and there MUST be one blank line after the use statements block. - 'single_line_after_imports' => true, - // Single-line comments and multi-line comments with only one line of actual content should use the `//` syntax. - 'single_line_comment_style' => ['comment_types'=>['hash']], - // Throwing exception must be done in single line. - 'single_line_throw' => true, - // Convert double quotes to single quotes for simple strings. - 'single_quote' => true, - // Each trait `use` must be done as single statement. - 'single_trait_insert_per_statement' => true, - // Fix whitespace after a semicolon. - 'space_after_semicolon' => ['remove_in_empty_for_expressions'=>true], - // Increment and decrement operators should be used if possible. - 'standardize_increment' => true, - // Replace all `<>` with `!=`. - 'standardize_not_equals' => true, - // A case should be followed by a colon and not a semicolon. - 'switch_case_semicolon_to_colon' => true, - // Removes extra spaces between colon and case value. - 'switch_case_space' => true, - // Switch case must not be ended with `continue` but with `break`. - 'switch_continue_to_break' => true, - // Standardize spaces around ternary operator. - 'ternary_operator_spaces' => true, - // Multi-line arrays, arguments list and parameters list must have a trailing comma. - 'trailing_comma_in_multiline' => true, - // Arrays should be formatted like function/method arguments, without leading or trailing single line space. - 'trim_array_spaces' => true, - // A single space or none should be around union type operator. - 'types_spaces' => true, - // Unary operators should be placed adjacent to their operands. - 'unary_operator_spaces' => true, - // Visibility MUST be declared on all properties and methods; `abstract` and `final` MUST be declared before the visibility; `static` MUST be declared after the visibility. - 'modifier_keywords' => true, - // In array declaration, there MUST be a whitespace after each comma. - 'whitespace_after_comma_in_array' => true, - // Write conditions in Yoda style (`true`), non-Yoda style (`['equal' => false, 'identical' => false, 'less_and_greater' => false]`) or ignore those conditions (`null`) based on configuration. - 'yoda_style' => false, - ]) - ->setFinder(PhpCsFixer\Finder::create() - ->exclude('.Build') - ->exclude('.github') - ->exclude('var') - ->in(__DIR__) - ); diff --git a/.phpstan.neon b/.phpstan.neon deleted file mode 100644 index a28b2d5..0000000 --- a/.phpstan.neon +++ /dev/null @@ -1,8 +0,0 @@ -parameters: - level: 9 - paths: - - 'Classes' - - 'Tests' - - 'ext_emconf.php' - ignoreErrors: - - '#Variable \$_EXTKEY might not be defined.#' diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..6d71de7 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,755 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2086,SC2046,SC2128,SC2178,SC2206 + +# Uncomment for debugging +# set -x + +# +# TYPO3 extension tea test runner based on docker. +# + +if [ "${CI}" != "true" ]; then + trap 'echo "runTests.sh SIGINT signal emitted";cleanUp;exit 2' SIGINT +fi + +printSummary() { + cleanUp + + echo "" >&2 + echo "###########################################################################" >&2 + echo "Result of ${TEST_SUITE}" >&2 + echo "Container runtime: ${CONTAINER_BIN}" >&2 + echo "Container suffix: ${SUFFIX}" + if [[ ${TEST_SUITE} =~ ^(npm|lintCss|lintJs)$ ]]; then + echo "NODE: ${IMAGE_NODEJS}" >&2 + else + echo "PHP: ${PHP_VERSION}" >&2 + echo "TYPO3: ${CORE_VERSION}" >&2 + fi + if [[ ${TEST_SUITE} =~ ^functional$ ]]; then + case "${DBMS}" in + mariadb|mysql|postgres) + echo "DBMS: ${DBMS} version ${DBMS_VERSION} driver ${DATABASE_DRIVER}" >&2 + ;; + sqlite) + echo "DBMS: ${DBMS} driver pdo_sqlite" >&2 + ;; + esac + fi + if [[ ${SUITE_EXIT_CODE} -eq 0 ]]; then + echo "SUCCESS" >&2 + else + echo "FAILURE" >&2 + fi + echo "###########################################################################" >&2 + echo "" >&2 + exit ${SUITE_EXIT_CODE} +} + +waitFor() { + local HOST=${1} + local PORT=${2} + local TESTCOMMAND=" + COUNT=0; + while ! nc -z ${HOST} ${PORT}; do + if [ \"\${COUNT}\" -gt 20 ]; then + echo \"Can not connect to ${HOST} port ${PORT}. Aborting.\"; + exit 1; + fi; + sleep 1; + COUNT=\$((COUNT + 1)); + done; + " + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name wait-for-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} /bin/sh -c "${TESTCOMMAND}" + # shellcheck disable=SC2181 # Disabled because we don‘t want to move the long line between the brackets + if [[ $? -gt 0 ]]; then + kill -SIGINT -$$ + fi +} + +cleanUp() { + ATTACHED_CONTAINERS=$(${CONTAINER_BIN} ps --filter network=${NETWORK} --format='{{.Names}}') + for ATTACHED_CONTAINER in ${ATTACHED_CONTAINERS}; do + ${CONTAINER_BIN} kill ${ATTACHED_CONTAINER} >/dev/null + done + ${CONTAINER_BIN} network rm ${NETWORK} >/dev/null +} + +handleDbmsOptions() { + # -a, -d, -i depend on each other. Validate input combinations and set defaults. + case ${DBMS} in + mariadb) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10.3" + if ! [[ ${DBMS_VERSION} =~ ^(10.3|10.4|10.5|10.6|10.7|10.8|10.9|10.10|10.11|11.0|11.1)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + mysql) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="8.0" + if ! [[ ${DBMS_VERSION} =~ ^(5.5|5.6|5.7|8.0)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + postgres) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + [ -z "${DBMS_VERSION}" ] && DBMS_VERSION="10" + if ! [[ ${DBMS_VERSION} =~ ^(10|11|12|13|14|15|16)$ ]]; then + echo "Invalid combination -d ${DBMS} -i ${DBMS_VERSION}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + sqlite) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid combination -d ${DBMS} -a ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + if [ -n "${DBMS_VERSION}" ]; then + echo "Invalid combination -d ${DBMS} -i ${DATABASE_DRIVER}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + *) + echo "Invalid option -d ${DBMS}" >&2 + echo >&2 + echo "Use \".Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + ;; + esac +} + +cleanCacheFiles() { + echo -n "Clean caches ... " + rm -rf \ + .Build/.cache \ + .php-cs-fixer.cache + echo "done" +} + +cleanTestFiles() { + # test related + echo -n "Clean test related files ... " + rm -rf \ + .Build/public/typo3temp/var/tests/ + echo "done" +} + +cleanRenderedDocumentationFiles() { + echo -n "Clean rendered documentation files ... " + rm -rf \ + Documentation-GENERATED-temp + echo "done" +} + +loadHelp() { + # Load help text into $HELP + read -r -d '' HELP < + Specifies which test suite to run + - cgl: Fixes the code style with the PHP Coding Standards Fixer (PHP-CS-Fixer). + - checkComposerNormalize: Checks the order of the composer.json entries. + - clean: clean up build, cache and testing related files and folders + - cleanCache: clean up cache related files and folders + - cleanRenderedDocumentation: clean up rendered documentation files and folders (Documentation-GENERATED-temp) + - cleanTests: clean up test related files and folders + - composer: "composer" with all remaining arguments dispatched. + - composerNormalize: Normalizes the composer.json. + - composerUnused: Finds unused Composer packages. + - composerUpdateMax: "composer update", with no platform.php config. + - composerUpdateMin: "composer update --prefer-lowest", with platform.php set to PHP version x.x.0. + - docsGenerate: Renders the extension ReST documentation. + - fix: Runs all automatic code style fixes. + - fixComposerNormalize: Normalizes the composer.json. + - functional: PHP functional tests + - lintCss: CSS file linting. Set -n for dry-run. + - lintJs: JavaScript file linting. Set -n for dry-run. + - lintJson: JSON linting + - lintPhp: PHP linting + - lintTypoScript: TypoScript linting + - lintXliff: XLIFF linting + - lintYaml: YAML linting + - npm: "npm" with all remaining arguments dispatched. + - phpCsFixer fixes code to follow the standards. + - phpmd: Checks code metrics in the PHP code using PHPMD. + - phpstan: PHPStan tests + - phpstanGenerateBaseline: regenerate PHPStan baseline, handy after PHPStan updates + - psr-verify: Verifies PSR-4 namespace correctness. + - rector: Fixes and upgrades the PHP code using Rector + - shellcheck: check runTests.sh for shell issues + - unit (default): PHP unit tests + - unitRandom: PHP unit tests in random order, add -o to use specific seed + - update: Updates existing typo3/core-testing-*:latest container images and removes dangling local volumes. + + -a + Only with -s functional|functionalDeprecated + Specifies to use another driver, following combinations are available: + - mysql + - mysqli (default) + - pdo_mysql + - mariadb + - mysqli (default) + - pdo_mysql + + -b + Container environment: + - docker + - podman (default) + + -d + Only with -s functional|functionalDeprecated + Specifies on which DBMS tests are performed + - mariadb: use mariadb + - mysql: use MySQL + - postgres: use postgres + - sqlite: (default): use sqlite + + -i version + Specify a specific database version + With "-d mariadb": + - 10.4 short-term, maintained until 2024-06-18 (default) + - 10.5 short-term, maintained until 2025-06-24 + - 10.6 long-term, maintained until 2026-06 + - 10.7 short-term, no longer maintained + - 10.8 short-term, maintained until 2023-05 + - 10.9 short-term, maintained until 2023-08 + - 10.10 short-term, maintained until 2023-11 + - 10.11 long-term, maintained until 2028-02 + - 11.0 development series + - 11.1 short-term development series, maintained until 2024-08 + - 11.2 short-term development series, maintained until 2024-11 + - 11.3 short-term development series, rolling release + - 11.4 long-term, maintained until 2029-05 + With "-d mysql": + - 8.0 maintained until 2026-04 (default) LTS + - 8.1 unmaintained since 2023-10 + - 8.2 unmaintained since 2024-01 + - 8.3 maintained until 2024-04 + - 8.4 maintained until 2032-04 LTS + With "-d postgres": + - 10 unmaintained since 2022-11-10 (default) + - 11 unmaintained since 2023-11-09 + - 12 maintained until 2024-11-14 + - 13 maintained until 2025-11-13 + - 14 maintained until 2026-11-12 + - 15 maintained until 2027-11-11 + - 16 maintained until 2028-11-09 + + -t <12.4|13.4> + Only with -s composerUpdateMin|composerUpdateMax + Specifies the TYPO3 CORE Version to be used + - 12.4: use TYPO3 v12 + - 13.4: (default) use TYPO3 v13 + + -p <8.1|8.2|8.3|8.4|8.5> + Specifies the PHP minor version to be used + - 8.1: use PHP 8.1 + - 8.2: (default) use PHP 8.2 + - 8.3: use PHP 8.3 + - 8.4: use PHP 8.4 + - 8.5: use PHP 8.5 + + -e "" (DEPRECATED). + Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom + Additional options to send to phpunit (unit & functional tests). For phpunit, + options starting with "--" must be added after options starting with "-". + Example -e "-d memory_limit=-1 --filter filterByValueRecursiveCorrectlyFiltersArray" to enable verbose output AND filter tests + named "canRetrieveValueWithGP" + DEPRECATED - pass arguments after the -- separator directly. For example, instead of + Build/Scripts/runTests.sh -s unit -e "--filter filterByValueRecursiveCorrectlyFiltersArray" + use + Build/Scripts/runTests.sh -s unit -- --filter filterByValueRecursiveCorrectlyFiltersArray + + -x + Only with -s functional|functionalDeprecated|unit|unitDeprecated|unitRandom + Send information to host instance for test or system under test break points. This is especially + useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port + can be selected with -y + + -y + Send xdebug information to a different port than default 9003 if an IDE like PhpStorm + is not listening on default port. + + -o + Only with -s unitRandom + Set specific random seed to replay a random run in this order again. The phpunit randomizer + outputs the used seed at the end (in gitlab core testing logs, too). Use that number to + replay the unit tests in that order. + + -n + Only with -s cgl|composerNormalize|npm|lintJs|lintCss + Activate dry-run in checks so they do not actively change files and only print broken ones. + + -u + Update existing typo3/core-testing-*:latest container images and remove dangling local volumes. + New images are published once in a while and only the latest ones are supported by core testing. + Use this if weird test errors occur. Also removes obsolete image versions of typo3/core-testing-*. + + -h + Show this help. + +Examples: + # Run all core unit tests using PHP 8.2 + ./Build/Scripts/runTests.sh + ./Build/Scripts/runTests.sh -s unit + + # Run all core units tests and enable xdebug (have a PhpStorm listening on port 9003!) + ./Build/Scripts/runTests.sh -x + + # Run unit tests in phpunit with xdebug on PHP 8.1 and filter for test filterByValueRecursiveCorrectlyFiltersArray + ./Build/Scripts/runTests.sh -x -p 8.1 -- --filter filterByValueRecursiveCorrectlyFiltersArray + + # Run functional tests in phpunit with a filtered test method name in a specified file + # example will currently execute two tests, both of which start with the search term + ./Build/Scripts/runTests.sh -s functional -- \ + --filter datetimeInstanceCanBePersistedToDatabaseIfTypeIsExplicitlySpecified \ + typo3/sysext/core/Tests/Functional/Database/ConnectionTest.php + + # Run functional tests on postgres with xdebug, php 8.1 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.1 -s functional -d postgres typo3/sysext/core/Tests/Functional/Authentication + + # Run functional tests on postgres 11 + ./Build/Scripts/runTests.sh -s functional -d postgres -i 11 +EOF +} + +# Test if at least one of the supported container binaries exists, else exit out with error +if ! type "docker" >/dev/null 2>&1 && ! type "podman" >/dev/null 2>&1; then + echo "This script relies on docker or podman. Please install at least one of them" >&2 + exit 1 +fi + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called, then go up two dirs. +THIS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +cd "$THIS_SCRIPT_DIR" || exit 1 +cd ../../ || exit 1 +ROOT_DIR="${PWD}" + +# Option defaults +TEST_SUITE="unit" +CORE_VERSION="13.4" +DBMS="sqlite" +DBMS_VERSION="" +PHP_VERSION="8.2" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +PHPUNIT_RANDOM="" +# CGLCHECK_DRY_RUN is a more generic dry-run switch not limited to CGL +CGLCHECK_DRY_RUN=0 +DATABASE_DRIVER="" +CONTAINER_BIN="" +COMPOSER_ROOT_VERSION="3.0.x-dev" +HELP_TEXT_NPM_CI="Now running \'npm ci --silent\'." +HELP_TEXT_NPM_FAILURE="npm clean-install has failed. Please run \'${0} -s npm ci\' to explore." +CONTAINER_INTERACTIVE="-it --init" +HOST_UID=$(id -u) +HOST_PID=$(id -g) +USERSET="" +SUFFIX="$RANDOM" +NETWORK="typo3-best-practices-tea-${SUFFIX}" +CI_PARAMS="${CI_PARAMS:-}" +CONTAINER_HOST="host.docker.internal" +# shellcheck disable=SC2034 # This variable will be needed when we try to clean up the root folder +PHPSTAN_CONFIG_FILE="Build/phpstan/phpstan.neon" + +# Option parsing updates above default vars +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=() +# Simple option parsing based on getopts (! not getopt) +while getopts "a:b:s:d:i:p:t:xy:o:nhu" OPT; do + case ${OPT} in + s) + TEST_SUITE=${OPTARG} + ;; + a) + DATABASE_DRIVER=${OPTARG} + ;; + b) + if ! [[ ${OPTARG} =~ ^(docker|podman)$ ]]; then + INVALID_OPTIONS+=("-b ${OPTARG}") + fi + CONTAINER_BIN=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + i) + DBMS_VERSION=${OPTARG} + ;; + p) + PHP_VERSION=${OPTARG} + if ! [[ ${PHP_VERSION} =~ ^(8.1|8.2|8.3|8.4|8.5)$ ]]; then + INVALID_OPTIONS+=("-p ${OPTARG}") + fi + ;; + t) + CORE_VERSION=${OPTARG} + if ! [[ ${CORE_VERSION} =~ ^(12.4|13.4)$ ]]; then + INVALID_OPTIONS+=("-t ${OPTARG}") + fi + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + o) + PHPUNIT_RANDOM="--random-order-seed=${OPTARG}" + ;; + n) + CGLCHECK_DRY_RUN=1 + ;; + h) + loadHelp + echo "${HELP}" + exit 0 + ;; + u) + TEST_SUITE=update + ;; + \?) + INVALID_OPTIONS+=("-${OPTARG}") + ;; + :) + INVALID_OPTIONS+=("-${OPTARG}") + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo ${I} >&2 + done + echo >&2 + echo "call \".Build/Scripts/runTests.sh -h\" to display help and valid options" + exit 1 +fi + +handleDbmsOptions + +# ENV var "CI" is set by gitlab-ci. Use it to force some CI details. +if [ "${CI}" == "true" ]; then + CONTAINER_INTERACTIVE="" +fi + +# determine default container binary to use: 1. podman 2. docker +if [[ -z "${CONTAINER_BIN}" ]]; then + if type "podman" >/dev/null 2>&1; then + CONTAINER_BIN="podman" + elif type "docker" >/dev/null 2>&1; then + CONTAINER_BIN="docker" + fi +fi + +if [ $(uname) != "Darwin" ] && [ ${CONTAINER_BIN} = "docker" ]; then + # Run docker jobs as current user to prevent permission issues. Not needed with podman. + USERSET="--user $HOST_UID" +fi + +if ! type ${CONTAINER_BIN} >/dev/null 2>&1; then + echo "Selected container environment \"${CONTAINER_BIN}\" not found. Please install \"${CONTAINER_BIN}\" or use -b option to select one." >&2 + exit 1 +fi + +# Create .cache dir: composer need this. +mkdir -p .cache +mkdir -p .Build/public/typo3temp/var/tests + +IMAGE_PHP="ghcr.io/typo3/core-testing-$(echo "php${PHP_VERSION}" | sed -e 's/\.//'):latest" +IMAGE_NODEJS="ghcr.io/typo3/core-testing-nodejs24:1.1" +IMAGE_SHELLCHECK="docker.io/koalaman/shellcheck:v0.11.0" +IMAGE_DOCS="ghcr.io/typo3-documentation/render-guides:0.36.0" +IMAGE_MARIADB="docker.io/mariadb:${DBMS_VERSION}" +IMAGE_MYSQL="docker.io/mysql:${DBMS_VERSION}" +IMAGE_POSTGRES="docker.io/postgres:${DBMS_VERSION}-alpine" + +# Remove handled options and leaving the rest in the line, so it can be passed raw to commands +shift $((OPTIND - 1)) + +${CONTAINER_BIN} network create ${NETWORK} >/dev/null + +if [ ${CONTAINER_BIN} = "docker" ]; then + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} --rm --network ${NETWORK} --add-host "${CONTAINER_HOST}:host-gateway" ${USERSET} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" +else + # podman + CONTAINER_HOST="host.containers.internal" + CONTAINER_COMMON_PARAMS="${CONTAINER_INTERACTIVE} ${CI_PARAMS} --rm --network ${NETWORK} -v ${ROOT_DIR}:${ROOT_DIR} -w ${ROOT_DIR}" +fi + +if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE="-e XDEBUG_MODE=off" + XDEBUG_CONFIG=" " +else + XDEBUG_MODE="-e XDEBUG_MODE=debug -e XDEBUG_TRIGGER=foo" + XDEBUG_CONFIG="client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal" +fi + +# Suite execution +case ${TEST_SUITE} in + cgl) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="composer check:php:cs-fixer" + else + COMMAND="composer fix:php:cs" + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + checkComposerNormalize) + COMMAND="composer normalize --no-check-lock --dry-run" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-normalize-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + clean) + cleanCacheFiles + cleanRenderedDocumentationFiles + cleanTestFiles + ;; + cleanCache) + cleanCacheFiles + ;; + cleanRenderedDocumentation) + cleanRenderedDocumentationFiles + ;; + cleanTests) + cleanTestFiles + ;; + composer) + COMMAND=(composer "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + composerUnused) + COMMAND="composer check:composer:unused" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-unused-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + composerUpdateMax) + # `dumpautoload` removed due to error with missing `composer.lock` file on publishing public assets. + COMMAND="composer config --unset platform.php; composer require --no-ansi --no-interaction --no-progress --no-install typo3/minimal:"^${CORE_VERSION}"; composer update --no-progress --no-interaction; composer show" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-max-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + composerUpdateMin) + # `dumpautoload` removed due to error with missing `composer.lock` file on publishing public assets. + COMMAND="composer config platform.php ${PHP_VERSION}.0; composer require --no-ansi --no-interaction --no-progress --no-install typo3/minimal:"^${CORE_VERSION}"; composer update --prefer-lowest --no-progress --no-interaction; composer show" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-min-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + docsGenerate) + mkdir -p Documentation-GENERATED-temp + chown -R ${HOST_UID}:${HOST_PID} Documentation-GENERATED-temp + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} --rm --pull always ${USERSET} -v "${ROOT_DIR}":/project ${IMAGE_DOCS} --config=Documentation --fail-on-log + SUITE_EXIT_CODE=$? + ;; + fix) + COMMAND="composer fix" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run fix:lint:js" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} /bin/sh -c "${COMMAND}" + COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run fix:lint:css" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + fixComposerNormalize) + COMMAND="composer normalize --no-check-lock" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-normalize-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + functional) + COMMAND=(.Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml --exclude-group not-${DBMS} "$@") + case ${DBMS} in + mariadb) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mariadb-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MARIADB} >/dev/null + waitFor mariadb-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mariadb-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + mysql) + echo "Using driver: ${DATABASE_DRIVER}" + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name mysql-func-${SUFFIX} --network ${NETWORK} -d -e MYSQL_ROOT_PASSWORD=funcp --tmpfs /var/lib/mysql/:rw,noexec,nosuid ${IMAGE_MYSQL} >/dev/null + waitFor mysql-func-${SUFFIX} 3306 + CONTAINERPARAMS="-e typo3DatabaseDriver=${DATABASE_DRIVER} -e typo3DatabaseName=func_test -e typo3DatabaseUsername=root -e typo3DatabaseHost=mysql-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + postgres) + ${CONTAINER_BIN} run --rm ${CI_PARAMS} --name postgres-func-${SUFFIX} --network ${NETWORK} -d -e POSTGRES_PASSWORD=funcp -e POSTGRES_USER=funcu --tmpfs /var/lib/postgresql/data:rw,noexec,nosuid ${IMAGE_POSTGRES} >/dev/null + waitFor postgres-func-${SUFFIX} 5432 + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_pgsql -e typo3DatabaseName=bamboo -e typo3DatabaseUsername=funcu -e typo3DatabaseHost=postgres-func-${SUFFIX} -e typo3DatabasePassword=funcp" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + sqlite) + CONTAINERPARAMS="-e typo3DatabaseDriver=pdo_sqlite" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name functional-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${CONTAINERPARAMS} ${IMAGE_PHP} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + esac + ;; + lintCss) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run check:lint:css" + else + COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run fix:lint:css" + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + lintJs) + if [ "${CGLCHECK_DRY_RUN}" -eq 1 ]; then + COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run check:lint:js" + else + COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run fix:lint:js" + fi + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + lintJson) + COMMAND="composer check:json:lint" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + lintPhp) + COMMAND="composer check:php:lint" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + lintTypoScript) + COMMAND="composer check:typoscript:lint" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + lintXliff) + COMMAND="php Build/Scripts/xliffLint.sh lint:xliff Resources/Private/Language" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name lintxliff-${SUFFIX} ${IMAGE_PHP} ${COMMAND} + SUITE_EXIT_CODE=$? + ;; + lintYaml) + COMMAND="composer check:yaml:lint" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + npm) + COMMAND=(npm "$@") + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} "${COMMAND[@]}" + SUITE_EXIT_CODE=$? + ;; + phpCsFixer) + if [ -n "${CGLCHECK_DRY_RUN}" ]; then + CGLCHECK_DRY_RUN="--dry-run --diff" + fi + COMMAND="php .Build/bin/php-cs-fixer fix -v ${CGLCHECK_DRY_RUN} --config=Build/php-cs-fixer/config.php" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpcsfixer-${SUFFIX} ${IMAGE_PHP} ${COMMAND} + SUITE_EXIT_CODE=$? + ;; + phpmd) + COMMAND=".Build/bin/phpmd Classes text Build/phpmd/config.xml" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name phpmd-${SUFFIX} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + phpstan) + COMMAND="composer check:php:stan" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + phpstanGenerateBaseline) + COMMAND="composer phpstan:baseline" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" + SUITE_EXIT_CODE=$? + ;; + psr-verify) + COMMAND="composer dumpautoload --optimize --strict-psr --no-plugins" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name psr-verify-${SUFFIX} ${IMAGE_PHP} ${COMMAND} + SUITE_EXIT_CODE=$? + ;; + shellcheck) + ${CONTAINER_BIN} run ${CONTAINER_INTERACTIVE} --rm --pull always ${USERSET} -v "${ROOT_DIR}":/project:ro ${IMAGE_SHELLCHECK} /project/Build/Scripts/runTests.sh + SUITE_EXIT_CODE=$? + ;; + unit) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} .Build/bin/phpunit -c Build/phpunit/UnitTests.xml "$@" + SUITE_EXIT_CODE=$? + ;; + unitRandom) + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name unit-random-${SUFFIX} ${XDEBUG_MODE} -e XDEBUG_CONFIG="${XDEBUG_CONFIG}" ${IMAGE_PHP} .Build/bin/phpunit -c Build/phpunit/UnitTests.xml --order-by=random ${PHPUNIT_RANDOM} "$@" + SUITE_EXIT_CODE=$? + ;; + update) + # pull typo3/core-testing-*:latest versions of those ones that exist locally + echo "> pull ghcr.io/typo3/core-testing-*:latest versions of those ones that exist locally" + ${CONTAINER_BIN} images ghcr.io/typo3/core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} ${CONTAINER_BIN} pull {} + echo "" + # remove "dangling" typo3/core-testing-* images (those tagged as ) + echo "> remove \"dangling\" ghcr.io/typo3/core-testing-* images (those tagged as )" + ${CONTAINER_BIN} images --filter "reference=ghcr.io/typo3/core-testing-*" --filter "dangling=true" --format "{{.ID}}" | xargs -I {} ${CONTAINER_BIN} rmi {} + echo "" + ;; + rector) + if [ -n "${CGLCHECK_DRY_RUN}" ]; then + CGLCHECK_DRY_RUN="--dry-run" + fi + COMMAND=".Build/bin/rector process ${CGLCHECK_DRY_RUN} --config=Build/rector/config.php" + ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name cgl-${SUFFIX} ${IMAGE_PHP} ${COMMAND} + SUITE_EXIT_CODE=$? + ;; + *) + loadHelp + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + if [ ${CONTAINER_BIN} = "docker" ]; then + ${CONTAINER_BIN} network rm ${NETWORK} >/dev/null + else + ${CONTAINER_BIN} network rm -f ${NETWORK} >/dev/null + fi + exit 1 + ;; +esac + +# Cleanup, print summary && exit with exitcode +printSummary diff --git a/Build/composer-unused/composer-unused.php b/Build/composer-unused/composer-unused.php new file mode 100644 index 0000000..0778153 --- /dev/null +++ b/Build/composer-unused/composer-unused.php @@ -0,0 +1,8 @@ + $config; diff --git a/Build/php-cs-fixer/config.php b/Build/php-cs-fixer/config.php new file mode 100644 index 0000000..a133432 --- /dev/null +++ b/Build/php-cs-fixer/config.php @@ -0,0 +1,21 @@ +setParallelConfig(ParallelConfigFactory::detect()); + +$config->addRules( + [ + 'native_function_invocation' => [ + 'include' => [], + 'scope' => 'all', + 'strict' => true, + ] + ] +); +$config->getFinder()->in('Classes')->in('Configuration')->in('Tests'); +return $config; diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon new file mode 100644 index 0000000..aab4991 --- /dev/null +++ b/Build/phpstan/phpstan-baseline.neon @@ -0,0 +1,2 @@ +parameters: + ignoreErrors: [] diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon new file mode 100644 index 0000000..621a961 --- /dev/null +++ b/Build/phpstan/phpstan.neon @@ -0,0 +1,64 @@ +includes: + - phpstan-baseline.neon + - ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon + - ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-execution-calls.neon + - ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-insecure-calls.neon + - ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-loose-calls.neon + +parameters: + phpVersion: 80100 + + level: 9 + + paths: + - ../../Classes + - ../../Configuration + - ../../Tests + + type_coverage: + return_type: 100 + param_type: 100 + property_type: 95 + + cognitive_complexity: + class: 10 + function: 5 + + type_perfect: + no_mixed_property: true + no_mixed_caller: true + null_over_false: true + narrow_param: true + narrow_return: true + + disallowedFunctionCalls: + - + function: + - 'var_dump()' + - 'xdebug_break()' + - 'debug()' + message: 'Use logging instead or remove if it was for debugging purposes.' + - + function: 'header()' + message: 'Use PSR-7 API instead' + + disallowedStaticCalls: + - + method: + - 'TYPO3\CMS\Extbase\Utility\DebuggerUtility::var_dump()' + - 'TYPO3\CMS\Core\Utility\DebugUtility::debug()' + message: 'Use logging instead or remove if it was for debugging purposes.' + + disallowedSuperglobals: + - + superglobal: + - '$_GET' + - '$_POST' + - '$_FILES' + - '$_SERVER' + message: 'Use PSR-7 API instead' + + ignoreErrors: + - + message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#' + path: '../../Tests/' diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..972cfa1 --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + ../../Classes + + + + + ../../Tests/Functional + + + diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml new file mode 100644 index 0000000..a68a108 --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,17 @@ + + + + + + + + + ../../Classes + + + + + ../../Tests/Unit + + + diff --git a/Build/rector/config.php b/Build/rector/config.php new file mode 100644 index 0000000..927dd8e --- /dev/null +++ b/Build/rector/config.php @@ -0,0 +1,65 @@ +withPaths([ + __DIR__ . '/../../Build/', + __DIR__ . '/../../Classes/', + __DIR__ . '/../../Configuration/', + __DIR__ . '/../../Tests/', + __DIR__ . '/../../ext_emconf.php', + ]) + ->withPhpSets() + ->withSets([ + // Rector sets + + // SetList::CODE_QUALITY, + // SetList::CODING_STYLE, + // SetList::DEAD_CODE, + // SetList::EARLY_RETURN, + // SetList::INSTANCEOF, + // SetList::NAMING, + // SetList::PRIVATIZATION, + // SetList::STRICT_BOOLEANS, + // SetList::TYPE_DECLARATION, + + // PHPUnit sets + + PHPUnitSetList::PHPUNIT_100, + // PHPUnitSetList::PHPUNIT_110, + // PHPUnitSetList::PHPUNIT_CODE_QUALITY, + + // TYPO3 Sets + // https://github.com/sabbelasichon/typo3-rector/blob/main/src/Set/Typo3LevelSetList.php + // https://github.com/sabbelasichon/typo3-rector/blob/main/src/Set/Typo3SetList.php + + Typo3SetList::CODE_QUALITY, + Typo3SetList::GENERAL, + + Typo3LevelSetList::UP_TO_TYPO3_12, + // Typo3LevelSetList::UP_TO_TYPO3_13, + ]) + // To have a better analysis from PHPStan, we teach it here some more things + ->withPHPStanConfigs([ + Typo3Option::PHPSTAN_FOR_RECTOR_PATH, + ]) + ->withRules([ + AddVoidReturnTypeWhereNoReturnRector::class, + ]) + ->withImportNames(true, true, false) + ->withConfiguredRule(ExtEmConfRector::class, [ + ExtEmConfRector::PHP_VERSION_CONSTRAINT => '8.1.0-8.5.99', + ExtEmConfRector::TYPO3_VERSION_CONSTRAINT => '12.4.0-14.3.99', + ExtEmConfRector::ADDITIONAL_VALUES_TO_BE_REMOVED => [], + ]) + ->withSkip([ + ]); diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3538386 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,7 @@ +# Code of Conduct + +This project uses the +[TYPO3 Code of Conduct](https://typo3.org/community/values/code-of-conduct). + +When you contribute to this project or interact with community members, +you agree to adhere to this code of conduct. diff --git a/Classes/Command/CrawlSitemapCommand.php b/Classes/Command/CrawlSitemapCommand.php index d898c82..064eddd 100644 --- a/Classes/Command/CrawlSitemapCommand.php +++ b/Classes/Command/CrawlSitemapCommand.php @@ -2,8 +2,6 @@ namespace Schliesser\Sitecrawler\Command; -use InvalidArgumentException; -use JsonException; use Schliesser\Sitecrawler\Exception\InvalidFormatException; use Schliesser\Sitecrawler\Exception\InvalidHeadersException; use Schliesser\Sitecrawler\Exception\InvalidUrlException; @@ -67,7 +65,7 @@ protected function configure(): void * @throws InvalidFormatException * @throws InvalidHeadersException * @throws InvalidUrlException - * @throws JsonException + * @throws \JsonException */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -75,7 +73,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $url = $input->getArgument('url'); if (!is_string($url)) { - throw new InvalidArgumentException('Argument "url" must be a string!', 1715513484); + throw new \InvalidArgumentException('Argument "url" must be a string!', 1715513484); } $io->writeln('Sitemap url: ' . $url, OutputInterface::VERBOSITY_VERBOSE); @@ -87,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Set headers from argument if ($headers = $input->getArgument('headers')) { if (!is_string($headers)) { - throw new InvalidArgumentException('Argument "headers" must be a json string!', 1715513588); + throw new \InvalidArgumentException('Argument "headers" must be a json string!', 1715513588); } $this->requestHeaders = array_merge($this->requestHeaders, $this->parseHeaders($headers)); $io->writeln('Headers: ' . var_export($this->requestHeaders, true), OutputInterface::VERBOSITY_DEBUG); @@ -111,24 +109,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Display url and sitemap count $sitemapCount = count($this->sitemaps); - $io->writeln('Found ' . count($this->urls) . ' url(s)' . ($sitemapCount ? ' in ' . $sitemapCount . ' sitemap(s)' : ''), - OutputInterface::VERBOSITY_VERBOSE); + $io->writeln( + 'Found ' . count($this->urls) . ' url(s)' . ($sitemapCount ? ' in ' . $sitemapCount . ' sitemap(s)' : ''), + OutputInterface::VERBOSITY_VERBOSE + ); // Return url list as txt/json when format option is set if ($format = $input->getOption('list')) { if (!is_string($format)) { - throw new InvalidArgumentException('Argument "list" must be a string!', 1715514158); - } - switch (strtolower($format)) { - case 'json': - $io->write(json_encode(['urls' => $this->urls, 'sitemaps' => $this->sitemaps], JSON_THROW_ON_ERROR)); - break; - case 'txt': - $io->listing($this->urls); - break; - default: - throw new InvalidFormatException('Invalid format for list "' . htmlspecialchars($format) . '"!', 1657265268452); + throw new \InvalidArgumentException('Argument "list" must be a string!', 1715514158); } + match (strtolower($format)) { + 'json' => $io->write(json_encode(['urls' => $this->urls, 'sitemaps' => $this->sitemaps], JSON_THROW_ON_ERROR)), + 'txt' => $io->listing($this->urls), + default => throw new InvalidFormatException('Invalid format for list "' . htmlspecialchars($format) . '"!', 1657265268452), + }; return 0; } @@ -278,7 +273,7 @@ protected function getArrayFromUrl(string $url): array } // Decode gzip compressed sitemaps - $isGzip = 0 === mb_strpos($data, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII'); + $isGzip = mb_strpos($data, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII') === 0; if ($isGzip) { $data = gzdecode($data); if (!is_string($data)) { @@ -299,7 +294,7 @@ protected function getArrayFromUrl(string $url): array // Convert SimpleXML Objects to associative array try { $array = json_decode(json_encode($xml, JSON_THROW_ON_ERROR) ?: '', true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { + } catch (\JsonException) { $this->errors[] = new Error(1715517272, 'Failed to transform xml data for url: "' . $url . '"'); return []; @@ -373,7 +368,7 @@ protected function testUrl(string $url): ?array * @return array * * @throws InvalidHeadersException - * @throws JsonException + * @throws \JsonException */ protected function parseHeaders(string $json): array { @@ -381,12 +376,12 @@ protected function parseHeaders(string $json): array $headers = []; if (!is_array($data)) { - throw new InvalidHeadersException('Invalid header json'); + throw new InvalidHeadersException('Invalid header json', 6068357587); } foreach ($data as $name => $value) { if (!is_string($name) || !is_string($value)) { - throw new InvalidHeadersException('Invalid header format'); + throw new InvalidHeadersException('Invalid header format', 5240210327); } $headers[$name] = $value; } diff --git a/Classes/Exception/Exception.php b/Classes/Exception/Exception.php index 4490321..d1fa211 100644 --- a/Classes/Exception/Exception.php +++ b/Classes/Exception/Exception.php @@ -2,6 +2,4 @@ namespace Schliesser\Sitecrawler\Exception; -class Exception extends \Exception -{ -} +class Exception extends \Exception {} diff --git a/Classes/Exception/InvalidFormatException.php b/Classes/Exception/InvalidFormatException.php index adb17f9..aba4716 100644 --- a/Classes/Exception/InvalidFormatException.php +++ b/Classes/Exception/InvalidFormatException.php @@ -2,6 +2,4 @@ namespace Schliesser\Sitecrawler\Exception; -class InvalidFormatException extends Exception -{ -} +class InvalidFormatException extends Exception {} diff --git a/Classes/Exception/InvalidHeadersException.php b/Classes/Exception/InvalidHeadersException.php index 7bfad26..d163ff2 100644 --- a/Classes/Exception/InvalidHeadersException.php +++ b/Classes/Exception/InvalidHeadersException.php @@ -2,6 +2,4 @@ namespace Schliesser\Sitecrawler\Exception; -class InvalidHeadersException extends Exception -{ -} +class InvalidHeadersException extends Exception {} diff --git a/Classes/Exception/InvalidUrlException.php b/Classes/Exception/InvalidUrlException.php index ac7154d..9996784 100644 --- a/Classes/Exception/InvalidUrlException.php +++ b/Classes/Exception/InvalidUrlException.php @@ -2,6 +2,4 @@ namespace Schliesser\Sitecrawler\Exception; -class InvalidUrlException extends Exception -{ -} +class InvalidUrlException extends Exception {} diff --git a/Classes/Helper/Error.php b/Classes/Helper/Error.php index d633af4..1b42e3b 100644 --- a/Classes/Helper/Error.php +++ b/Classes/Helper/Error.php @@ -6,9 +6,7 @@ class Error { - public function __construct(protected readonly int $code, protected readonly string $message) - { - } + public function __construct(protected readonly int $code, protected readonly string $message) {} public function getCode(): int { diff --git a/Tests/Functional/Command/CrawlSitemapCommandTest.php b/Tests/Functional/Command/CrawlSitemapCommandTest.php index f008166..ccdf32a 100644 --- a/Tests/Functional/Command/CrawlSitemapCommandTest.php +++ b/Tests/Functional/Command/CrawlSitemapCommandTest.php @@ -6,6 +6,7 @@ use donatj\MockWebServer\MockWebServer; use donatj\MockWebServer\Response; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Test; use Schliesser\Sitecrawler\Command\CrawlSitemapCommand; use Schliesser\Sitecrawler\Exception\InvalidFormatException; @@ -14,6 +15,7 @@ use Symfony\Component\Console\Tester\CommandTester; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; +#[CoversClass(CrawlSitemapCommandTest::class)] class CrawlSitemapCommandTest extends FunctionalTestCase { protected array $testExtensionsToLoad = ['typo3conf/ext/sitecrawler']; diff --git a/Tests/FunctionalTests.xml b/Tests/FunctionalTests.xml index 5de65aa..07235e1 100644 --- a/Tests/FunctionalTests.xml +++ b/Tests/FunctionalTests.xml @@ -1,5 +1,18 @@ - + ./ diff --git a/Tests/Unit/Environment/ExtensionTest.php b/Tests/Unit/Environment/ExtensionTest.php new file mode 100644 index 0000000..82f23a0 --- /dev/null +++ b/Tests/Unit/Environment/ExtensionTest.php @@ -0,0 +1,25 @@ +getMajorVersion(); + self::assertContains( + $currentVersion, + $supportedVersions + ); + } +} diff --git a/composer.json b/composer.json index 13bb20c..4a52e86 100644 --- a/composer.json +++ b/composer.json @@ -1,27 +1,47 @@ { "name": "schliesser/sitecrawler", "description": "TYPO3 sitemap crawler", - "type": "typo3-cms-extension", "license": "GPL-2.0-or-later", + "type": "typo3-cms-extension", "authors": [ { "name": "André Buchmann", "email": "andy.schliesser@gmail.com", - "role": "Developer" + "role": "maintainer" } ], + "homepage": "https://extensions.typo3.org/extension/sitecrawler", + "support": { + "issues": "https://github.com/schliesser/sitecrawler/issues", + "source": "https://github.com/schliesser/sitecrawler", + "docs": "https://github.com/schliesser/sitecrawler/blob/main/README.md" + }, "require": { "php": "^8.1", "ext-simplexml": "*", "ext-zlib": "*", - "typo3/cms-core": "^12.4 || ^13.4 || ^14.0 || 14.*.*@dev" + "typo3/cms-core": "^12.4 || ^13.4 || 14.*.*@dev || ^14.0" }, "require-dev": { - "donatj/mock-webserver": "^2.7", - "friendsofphp/php-cs-fixer": "^3.0", - "php-parallel-lint/php-parallel-lint": "^1.2", - "phpstan/phpstan": "^2.1", + "a9f/typo3-fractor": "^0.5.10", + "donatj/mock-webserver": "^2.10.0", + "ergebnis/composer-normalize": "^2.50", + "friendsofphp/php-cs-fixer": "^3.94.2", + "icanhazstring/composer-unused": "^0.9.6", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "rector/type-perfect": "^2.1", + "saschaegerer/phpstan-typo3": "^3.0", + "spaze/phpstan-disallowed-calls": "^4.9", + "ssch/typo3-rector": "^3.13", + "ssch/typo3-rector-testing-framework": "^3.0", + "tomasvotruba/cognitive-complexity": "^1.0", + "tomasvotruba/type-coverage": "^2.1", "typo3/cms-composer-installers": "^5.0", + "typo3/coding-standards": "^0.8.0", "typo3/testing-framework": "^8.0 || ^9.3" }, "autoload": { @@ -35,53 +55,123 @@ } }, "config": { - "vendor-dir": ".Build/vendor", - "bin-dir": ".Build/bin", "allow-plugins": { + "a9f/fractor-extension-installer": true, + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true, "typo3/class-alias-loader": true, "typo3/cms-composer-installers": true }, - "sort-packages": true + "bin-dir": ".Build/bin", + "lock": false, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "vendor-dir": ".Build/vendor" + }, + "extra": { + "typo3/cms": { + "extension-key": "sitecrawler", + "web-dir": ".Build/public" + } }, "scripts": { - "test": [ - "@test-php-lint", - "@test-php-cs-fixer", - "@test-phpstan", - "@test-functional" + "check:composer:normalize": "@composer normalize --no-check-lock --dry-run", + "check:composer:psr-verify": "@composer dumpautoload --optimize --strict-psr --no-plugins", + "check:composer:unused": "composer-unused --configuration=Build/composer-unused/composer-unused.php", + "check:coverage": [ + "@check:coverage:unit", + "@check:coverage:functional" + ], + "check:coverage:functional": [ + "@check:tests:create-directories", + "@coverage:create-directories", + "phpunit -c Build/phpunit/FunctionalTests.xml --coverage-php=build/coverage/functional.cov" + ], + "check:coverage:merge": [ + "@coverage:create-directories", + "@php tools/phpcov merge --clover=build/logs/clover.xml build/coverage/" ], - "test-php-lint": [ - ".Build/bin/parallel-lint ./Classes/", - ".Build/bin/parallel-lint ./Tests/" + "check:coverage:unit": [ + "@coverage:create-directories", + "phpunit -c Build/phpunit/UnitTests.xml --coverage-php=build/coverage/unit.cov" ], - "test-php-cs-fixer": [ - "php-cs-fixer fix -v --dry-run --using-cache no --diff" + "check:php:cs-fixer": "php-cs-fixer fix --config ./Build/php-cs-fixer/config.php -v --dry-run --diff", + "check:php:lint": "parallel-lint *.php Build Classes Configuration Tests", + "check:php:rector": "rector process --dry-run --config=./Build/rector/config.php", + "check:php:stan": "phpstan --no-progress -v --configuration=Build/phpstan/phpstan.neon", + "check:static": [ + "@check:composer:normalize", + "@check:composer:unused", + "@check:json:lint", + "@check:php:lint", + "@check:composer:psr-verify", + "@check:php:cs-fixer", + "@check:php:rector", + "@check:php:stan", + "@check:yaml:lint" ], - "test-php-cs-fixer-fix": [ - "php-cs-fixer fix -v --using-cache no" + "check:tests:create-directories": "mkdir -p .Build/public/typo3temp/var/tests", + "check:tests:functional": [ + "@check:tests:create-directories", + "phpunit -c Build/phpunit/FunctionalTests.xml" ], - "test-functional": [ - "[ -e .Build/bin/phpunit ] || composer update", - "TYPO3_PATH_WEB=$PWD/.Build typo3DatabaseDriver=pdo_sqlite typo3DatabaseName=test .Build/bin/phpunit -c Tests/FunctionalTests.xml Tests/Functional" + "check:tests:unit": "phpunit -c Build/phpunit/UnitTests.xml", + "check:yaml:lint": "find . ! -path '*.Build/*' ! -path '*node_modules/*' \\( -name '*.yaml' -o -name '*.yml' \\) | xargs -r php ./.Build/bin/yaml-lint", + "coverage:create-directories": "mkdir -p build/coverage build/logs", + "fix": [ + "@fix:composer:normalize", + "@fix:php" ], - "test-phpstan": [ - "phpstan analyse -c .phpstan.neon" + "fix:composer:normalize": "@composer normalize --no-check-lock", + "fix:php": [ + "@fix:php:rector", + "@fix:php:cs-fixer" ], + "fix:php:cs-fixer": "php-cs-fixer fix --config ./Build/php-cs-fixer/config.php", + "fix:php:rector": "rector process --config=./Build/rector/config.php", + "phpstan:baseline": "phpstan --generate-baseline=Build/phpstan/phpstan-baseline.neon --allow-empty-baseline --configuration=Build/phpstan/phpstan.neon", "prepare-release": [ - "rm .gitignore", "rm -rf .Build", + "rm -rf .ddev", "rm -rf .github", + "rm -rf Build", "rm -rf Tests", + "rm -rf tools", "rm .editorconfig", "rm .gitattributes", - "rm .php-cs-fixer.php" + "rm .gitignore", + "rm phive.xml" + ], + "check:tests:functional:local": [ + "TYPO3_PATH_WEB=$PWD/.Build typo3DatabaseDriver=pdo_sqlite typo3DatabaseName=test phpunit -c Build/phpunit/FunctionalTests.xml" ] }, - "extra": { - "typo3/cms": { - "cms-package-dir": "{$vendor-dir}/typo3/cms", - "web-dir": ".Build/Web", - "extension-key": "sitecrawler" - } + "scripts-descriptions": { + "check:composer:normalize": "Checks the composer.json.", + "check:composer:psr-verify": "Verifies PSR-4 namespace correctness.", + "check:composer:unused": "Finds unused Composer packages required in composer.json.", + "check:coverage:functional": "Generates the code coverage report for functional tests.", + "check:coverage:merge": "Merges the code coverage reports for unit and functional tests.", + "check:coverage:unit": "Generates the code coverage report for unit tests.", + "check:php": "Runs all static checks for the PHP files.", + "check:php:cs-fixer": "Checks the code style with the PHP Coding Standards Fixer (PHP-CS-Fixer).", + "check:php:lint": "Lints the PHP files for syntax errors.", + "check:php:rector": "Checks for code for changes by Rector.", + "check:php:stan": "Checks the PHP types using PHPStan.", + "check:static": "Runs all static code checks (syntax, style, types).", + "check:tests:create-directories": "Creates the directories required to smoothely run the functional tests.", + "check:tests:functional": "Runs the functional tests.", + "check:tests:unit": "Runs the unit tests.", + "check:yaml:lint": "Lints the YAML files.", + "coverage:create-directories": "Creates the directories needed for recording and merging the code coverage reports.", + "fix": "Runs all automatic code style fixes.", + "fix:composer:normalize": "Normalizes composer.json file content.", + "fix:php": "Runs all fixers for the PHP code.", + "fix:php:cs-fixer": "Fixes the code style with PHP-CS-Fixer.", + "fix:php:rector": "Updates the code with Rector.", + "phpstan:baseline": "Updates the PHPStan baseline file to match the code.", + "prepare-release": "Removes development-only files in preparation of a TER release." } } diff --git a/ext_emconf.php b/ext_emconf.php index 5cedad3..77204a2 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -7,7 +7,6 @@ 'author' => 'André Buchmann', 'author_email' => 'andy.schliesser@gmail.com', 'state' => 'stable', - 'clearCacheOnLoad' => false, 'version' => '3.0.2', 'constraints' => [ 'depends' => [ diff --git a/phive.xml b/phive.xml new file mode 100644 index 0000000..93d62fd --- /dev/null +++ b/phive.xml @@ -0,0 +1,4 @@ + + + + From 2e5c3f6f1424c751ddd8ead1e41d38de7e267820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Buchmann?= Date: Thu, 2 Apr 2026 22:01:31 +0200 Subject: [PATCH 2/3] [TASK] Drop php 8.1 support --- .github/workflows/ci.yml | 10 +- .gitignore | 3 +- Build/Scripts/runTests.sh | 23 +- Build/fractor/config.php | 15 + Build/phpstan/phpstan.neon | 7 +- Build/rector/config.php | 3 +- .../Command/CrawlSitemapCommandTest.php | 11 +- Tests/Functional/Fixtures/sitemap-1.xml | 19 +- Tests/Functional/Fixtures/sitemap-2.xml | 28 +- Tests/Functional/Fixtures/sitemap-index-1.xml | 17 +- Tests/Functional/Fixtures/sitemap-index-2.xml | 26 +- .../Functional/Fixtures/sitemap-index-gz.xml | 17 +- Tests/FunctionalTests.xml | 42 +-- composer.json | 350 +++++++++--------- 14 files changed, 276 insertions(+), 295 deletions(-) create mode 100644 Build/fractor/config.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a4cca1..4d27600 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,6 @@ jobs: fail-fast: false matrix: php-version: - - "8.1" - "8.2" - "8.3" - "8.4" @@ -57,6 +56,7 @@ jobs: - "php:rector" - "php:stan" - "yaml:lint" + - "fractor" static-code-analysis: name: Static code quality checks runs-on: ubuntu-24.04 @@ -126,10 +126,10 @@ jobs: matrix: include: - typo3-version: "12.4" - php-version: "8.1" + php-version: "8.2" composer-dependencies: Min - typo3-version: "12.4" - php-version: "8.1" + php-version: "8.2" composer-dependencies: Max - typo3-version: "12.4" php-version: "8.5" @@ -171,10 +171,10 @@ jobs: matrix: include: - typo3-version: "12.4" - php-version: "8.1" + php-version: "8.2" composer-dependencies: Min - typo3-version: "12.4" - php-version: "8.1" + php-version: "8.2" composer-dependencies: Max - typo3-version: "12.4" php-version: "8.5" diff --git a/.gitignore b/.gitignore index 651c17a..f029b6f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,8 @@ composer.lock /.php-cs-fixer.cache /.phpunit.result.cache /Documentation-GENERATED-temp/ -/build +/build/coverage +/build/logs /generate-documentation.sh /nbproject /var diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 6d71de7..574698d 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -277,9 +277,8 @@ Options: - 12.4: use TYPO3 v12 - 13.4: (default) use TYPO3 v13 - -p <8.1|8.2|8.3|8.4|8.5> + -p <8.2|8.3|8.4|8.5> Specifies the PHP minor version to be used - - 8.1: use PHP 8.1 - 8.2: (default) use PHP 8.2 - 8.3: use PHP 8.3 - 8.4: use PHP 8.4 @@ -332,8 +331,8 @@ Examples: # Run all core units tests and enable xdebug (have a PhpStorm listening on port 9003!) ./Build/Scripts/runTests.sh -x - # Run unit tests in phpunit with xdebug on PHP 8.1 and filter for test filterByValueRecursiveCorrectlyFiltersArray - ./Build/Scripts/runTests.sh -x -p 8.1 -- --filter filterByValueRecursiveCorrectlyFiltersArray + # Run unit tests in phpunit with xdebug on PHP 8.2 and filter for test filterByValueRecursiveCorrectlyFiltersArray + ./Build/Scripts/runTests.sh -x -p 8.2 -- --filter filterByValueRecursiveCorrectlyFiltersArray # Run functional tests in phpunit with a filtered test method name in a specified file # example will currently execute two tests, both of which start with the search term @@ -341,8 +340,8 @@ Examples: --filter datetimeInstanceCanBePersistedToDatabaseIfTypeIsExplicitlySpecified \ typo3/sysext/core/Tests/Functional/Database/ConnectionTest.php - # Run functional tests on postgres with xdebug, php 8.1 and execute a restricted set of tests - ./Build/Scripts/runTests.sh -x -p 8.1 -s functional -d postgres typo3/sysext/core/Tests/Functional/Authentication + # Run functional tests on postgres with xdebug, php 8.2 and execute a restricted set of tests + ./Build/Scripts/runTests.sh -x -p 8.2 -s functional -d postgres typo3/sysext/core/Tests/Functional/Authentication # Run functional tests on postgres 11 ./Build/Scripts/runTests.sh -s functional -d postgres -i 11 @@ -417,13 +416,13 @@ while getopts "a:b:s:d:i:p:t:xy:o:nhu" OPT; do ;; p) PHP_VERSION=${OPTARG} - if ! [[ ${PHP_VERSION} =~ ^(8.1|8.2|8.3|8.4|8.5)$ ]]; then + if ! [[ ${PHP_VERSION} =~ ^(8.2|8.3|8.4|8.5)$ ]]; then INVALID_OPTIONS+=("-p ${OPTARG}") fi ;; t) CORE_VERSION=${OPTARG} - if ! [[ ${CORE_VERSION} =~ ^(12.4|13.4)$ ]]; then + if ! [[ ${CORE_VERSION} =~ ^(12.4|13.4|14.1)$ ]]; then INVALID_OPTIONS+=("-t ${OPTARG}") fi ;; @@ -568,13 +567,13 @@ case ${TEST_SUITE} in ;; composerUpdateMax) # `dumpautoload` removed due to error with missing `composer.lock` file on publishing public assets. - COMMAND="composer config --unset platform.php; composer require --no-ansi --no-interaction --no-progress --no-install typo3/minimal:"^${CORE_VERSION}"; composer update --no-progress --no-interaction; composer show" + COMMAND="composer config --unset platform.php; composer require --no-ansi --no-interaction --no-progress --no-install typo3/minimal:"^${CORE_VERSION}" --ignore-platform-req=ext-sockets; composer update --no-progress --no-interaction --ignore-platform-req=ext-sockets; composer show" ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-max-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" SUITE_EXIT_CODE=$? ;; composerUpdateMin) # `dumpautoload` removed due to error with missing `composer.lock` file on publishing public assets. - COMMAND="composer config platform.php ${PHP_VERSION}.0; composer require --no-ansi --no-interaction --no-progress --no-install typo3/minimal:"^${CORE_VERSION}"; composer update --prefer-lowest --no-progress --no-interaction; composer show" + COMMAND="composer config platform.php ${PHP_VERSION}.0; composer require --no-ansi --no-interaction --no-progress --no-install typo3/minimal:"^${CORE_VERSION}" --ignore-platform-req=ext-sockets; composer update --prefer-lowest --no-progress --no-interaction --ignore-platform-req=ext-sockets; composer show" ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-install-min-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" SUITE_EXIT_CODE=$? ;; @@ -587,10 +586,6 @@ case ${TEST_SUITE} in fix) COMMAND="composer fix" ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name composer-command-${SUFFIX} -e COMPOSER_CACHE_DIR=.cache/composer -e COMPOSER_ROOT_VERSION=${COMPOSER_ROOT_VERSION} ${IMAGE_PHP} /bin/sh -c "${COMMAND}" - COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run fix:lint:js" - ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} /bin/sh -c "${COMMAND}" - COMMAND="echo ${HELP_TEXT_NPM_CI}; npm ci --silent || { echo ${HELP_TEXT_NPM_FAILURE}; exit 1; } && npm run fix:lint:css" - ${CONTAINER_BIN} run ${CONTAINER_COMMON_PARAMS} --name npm-command-${SUFFIX} ${IMAGE_NODEJS} /bin/sh -c "${COMMAND}" SUITE_EXIT_CODE=$? ;; fixComposerNormalize) diff --git a/Build/fractor/config.php b/Build/fractor/config.php new file mode 100644 index 0000000..d19fa60 --- /dev/null +++ b/Build/fractor/config.php @@ -0,0 +1,15 @@ +withPaths([ + __DIR__ . '/../../Configuration', + __DIR__ . '/../../Tests', + ]) + ->withSets([ + Typo3LevelSetList::UP_TO_TYPO3_12, + ]); diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon index 621a961..4224aff 100644 --- a/Build/phpstan/phpstan.neon +++ b/Build/phpstan/phpstan.neon @@ -6,7 +6,7 @@ includes: - ../../.Build/vendor/spaze/phpstan-disallowed-calls/disallowed-loose-calls.neon parameters: - phpVersion: 80100 + phpVersion: 80200 level: 9 @@ -57,8 +57,3 @@ parameters: - '$_FILES' - '$_SERVER' message: 'Use PSR-7 API instead' - - ignoreErrors: - - - message: '#^Call to static method PHPUnit\\Framework\\Assert\:\:assertInstanceOf\(\) .* will always evaluate to#' - path: '../../Tests/' diff --git a/Build/rector/config.php b/Build/rector/config.php index 927dd8e..0cba4eb 100644 --- a/Build/rector/config.php +++ b/Build/rector/config.php @@ -46,7 +46,6 @@ Typo3SetList::GENERAL, Typo3LevelSetList::UP_TO_TYPO3_12, - // Typo3LevelSetList::UP_TO_TYPO3_13, ]) // To have a better analysis from PHPStan, we teach it here some more things ->withPHPStanConfigs([ @@ -57,7 +56,7 @@ ]) ->withImportNames(true, true, false) ->withConfiguredRule(ExtEmConfRector::class, [ - ExtEmConfRector::PHP_VERSION_CONSTRAINT => '8.1.0-8.5.99', + ExtEmConfRector::PHP_VERSION_CONSTRAINT => '8.2.0-8.5.99', ExtEmConfRector::TYPO3_VERSION_CONSTRAINT => '12.4.0-14.3.99', ExtEmConfRector::ADDITIONAL_VALUES_TO_BE_REMOVED => [], ]) diff --git a/Tests/Functional/Command/CrawlSitemapCommandTest.php b/Tests/Functional/Command/CrawlSitemapCommandTest.php index ccdf32a..f8ecd29 100644 --- a/Tests/Functional/Command/CrawlSitemapCommandTest.php +++ b/Tests/Functional/Command/CrawlSitemapCommandTest.php @@ -15,7 +15,7 @@ use Symfony\Component\Console\Tester\CommandTester; use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase; -#[CoversClass(CrawlSitemapCommandTest::class)] +#[CoversClass(CrawlSitemapCommand::class)] class CrawlSitemapCommandTest extends FunctionalTestCase { protected array $testExtensionsToLoad = ['typo3conf/ext/sitecrawler']; @@ -55,15 +55,6 @@ public function invalidUrlThrowsInvalidUrlException(): void $this->commandTester->execute(['url' => 'foo-bar']); } - #[Test] - public function sitemapWithSingleUrlGist(): void - { - $this->commandTester->execute( - ['url' => 'https://gist.githubusercontent.com/schliesser/042fe0d0780bde3f8223a74f25fbb3f1/raw/sitemap-1.xml'] - ); - self::assertStringContainsString('Completed successfully!', $this->commandTester->getDisplay()); - } - #[Test] public function sitemapWithSingleUrl(): void { diff --git a/Tests/Functional/Fixtures/sitemap-1.xml b/Tests/Functional/Fixtures/sitemap-1.xml index 618e461..826dec5 100644 --- a/Tests/Functional/Fixtures/sitemap-1.xml +++ b/Tests/Functional/Fixtures/sitemap-1.xml @@ -1,10 +1,11 @@ - - - - - - http://127.0.0.1:1337/ - 2020-06-12T11:49:26+02:00 - - + + + + + http://127.0.0.1:1337/ + 2020-06-12T11:49:26+02:00 + diff --git a/Tests/Functional/Fixtures/sitemap-2.xml b/Tests/Functional/Fixtures/sitemap-2.xml index 2ad64d1..d1c6841 100644 --- a/Tests/Functional/Fixtures/sitemap-2.xml +++ b/Tests/Functional/Fixtures/sitemap-2.xml @@ -1,15 +1,15 @@ - - - - - - http://127.0.0.1:1337/ - 2020-06-12T11:49:26+02:00 - - - - http://127.0.0.1:1337/page - 2020-06-12T11:49:26+02:00 - - + + + + + http://127.0.0.1:1337/ + 2020-06-12T11:49:26+02:00 + + + http://127.0.0.1:1337/page + 2020-06-12T11:49:26+02:00 + diff --git a/Tests/Functional/Fixtures/sitemap-index-1.xml b/Tests/Functional/Fixtures/sitemap-index-1.xml index c9f84ad..0ac8c74 100644 --- a/Tests/Functional/Fixtures/sitemap-index-1.xml +++ b/Tests/Functional/Fixtures/sitemap-index-1.xml @@ -1,10 +1,9 @@ - - - - - - http://127.0.0.1:1337/sitemap-1.xml - 2020-06-30T10:36:11+02:00 - - + + + + + http://127.0.0.1:1337/sitemap-1.xml + 2020-06-30T10:36:11+02:00 + diff --git a/Tests/Functional/Fixtures/sitemap-index-2.xml b/Tests/Functional/Fixtures/sitemap-index-2.xml index fce7c28..fc35cca 100644 --- a/Tests/Functional/Fixtures/sitemap-index-2.xml +++ b/Tests/Functional/Fixtures/sitemap-index-2.xml @@ -1,15 +1,13 @@ - - - - - - http://127.0.0.1:1337/sitemap-1.xml - 2020-06-30T10:36:11+02:00 - - - - http://127.0.0.1:1337/sitemap-2.xml - 2019-09-04T14:54:32+02:00 - - + + + + + http://127.0.0.1:1337/sitemap-1.xml + 2020-06-30T10:36:11+02:00 + + + http://127.0.0.1:1337/sitemap-2.xml + 2019-09-04T14:54:32+02:00 + diff --git a/Tests/Functional/Fixtures/sitemap-index-gz.xml b/Tests/Functional/Fixtures/sitemap-index-gz.xml index c587bd0..b423d24 100644 --- a/Tests/Functional/Fixtures/sitemap-index-gz.xml +++ b/Tests/Functional/Fixtures/sitemap-index-gz.xml @@ -1,10 +1,9 @@ - - - - - - http://127.0.0.1:1337/sitemap-2.xml.gz - 2020-06-30T10:36:11+02:00 - - + + + + + http://127.0.0.1:1337/sitemap-2.xml.gz + 2020-06-30T10:36:11+02:00 + diff --git a/Tests/FunctionalTests.xml b/Tests/FunctionalTests.xml index 07235e1..c6ea298 100644 --- a/Tests/FunctionalTests.xml +++ b/Tests/FunctionalTests.xml @@ -1,30 +1,18 @@ - - - ./ - - - - - - - - - ../Classes - - + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="true" bootstrap="../.Build/vendor/typo3/testing-framework/Resources/Core/Build/FunctionalTestsBootstrap.php" colors="true" stopOnError="false" stopOnFailure="false" stopOnIncomplete="false" stopOnSkipped="false" beStrictAboutTestsThatDoNotTestAnything="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd" cacheDirectory=".phpunit.cache" requireCoverageMetadata="false"> + + + ./ + + + + + + + + + ../Classes + + diff --git a/composer.json b/composer.json index 4a52e86..4854c9e 100644 --- a/composer.json +++ b/composer.json @@ -1,177 +1,177 @@ { - "name": "schliesser/sitecrawler", - "description": "TYPO3 sitemap crawler", - "license": "GPL-2.0-or-later", - "type": "typo3-cms-extension", - "authors": [ - { - "name": "André Buchmann", - "email": "andy.schliesser@gmail.com", - "role": "maintainer" - } - ], - "homepage": "https://extensions.typo3.org/extension/sitecrawler", - "support": { - "issues": "https://github.com/schliesser/sitecrawler/issues", - "source": "https://github.com/schliesser/sitecrawler", - "docs": "https://github.com/schliesser/sitecrawler/blob/main/README.md" - }, - "require": { - "php": "^8.1", - "ext-simplexml": "*", - "ext-zlib": "*", - "typo3/cms-core": "^12.4 || ^13.4 || 14.*.*@dev || ^14.0" - }, - "require-dev": { - "a9f/typo3-fractor": "^0.5.10", - "donatj/mock-webserver": "^2.10.0", - "ergebnis/composer-normalize": "^2.50", - "friendsofphp/php-cs-fixer": "^3.94.2", - "icanhazstring/composer-unused": "^0.9.6", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^2.1.44", - "phpstan/phpstan-phpunit": "^2.0", - "phpstan/phpstan-strict-rules": "^2.0", - "rector/type-perfect": "^2.1", - "saschaegerer/phpstan-typo3": "^3.0", - "spaze/phpstan-disallowed-calls": "^4.9", - "ssch/typo3-rector": "^3.13", - "ssch/typo3-rector-testing-framework": "^3.0", - "tomasvotruba/cognitive-complexity": "^1.0", - "tomasvotruba/type-coverage": "^2.1", - "typo3/cms-composer-installers": "^5.0", - "typo3/coding-standards": "^0.8.0", - "typo3/testing-framework": "^8.0 || ^9.3" - }, - "autoload": { - "psr-4": { - "Schliesser\\Sitecrawler\\": "Classes" - } - }, - "autoload-dev": { - "psr-4": { - "Schliesser\\Sitecrawler\\Tests\\": "Tests" - } - }, - "config": { - "allow-plugins": { - "a9f/fractor-extension-installer": true, - "ergebnis/composer-normalize": true, - "phpstan/extension-installer": true, - "typo3/class-alias-loader": true, - "typo3/cms-composer-installers": true - }, - "bin-dir": ".Build/bin", - "lock": false, - "preferred-install": { - "*": "dist" - }, - "sort-packages": true, - "vendor-dir": ".Build/vendor" - }, - "extra": { - "typo3/cms": { - "extension-key": "sitecrawler", - "web-dir": ".Build/public" - } - }, - "scripts": { - "check:composer:normalize": "@composer normalize --no-check-lock --dry-run", - "check:composer:psr-verify": "@composer dumpautoload --optimize --strict-psr --no-plugins", - "check:composer:unused": "composer-unused --configuration=Build/composer-unused/composer-unused.php", - "check:coverage": [ - "@check:coverage:unit", - "@check:coverage:functional" - ], - "check:coverage:functional": [ - "@check:tests:create-directories", - "@coverage:create-directories", - "phpunit -c Build/phpunit/FunctionalTests.xml --coverage-php=build/coverage/functional.cov" - ], - "check:coverage:merge": [ - "@coverage:create-directories", - "@php tools/phpcov merge --clover=build/logs/clover.xml build/coverage/" - ], - "check:coverage:unit": [ - "@coverage:create-directories", - "phpunit -c Build/phpunit/UnitTests.xml --coverage-php=build/coverage/unit.cov" - ], - "check:php:cs-fixer": "php-cs-fixer fix --config ./Build/php-cs-fixer/config.php -v --dry-run --diff", - "check:php:lint": "parallel-lint *.php Build Classes Configuration Tests", - "check:php:rector": "rector process --dry-run --config=./Build/rector/config.php", - "check:php:stan": "phpstan --no-progress -v --configuration=Build/phpstan/phpstan.neon", - "check:static": [ - "@check:composer:normalize", - "@check:composer:unused", - "@check:json:lint", - "@check:php:lint", - "@check:composer:psr-verify", - "@check:php:cs-fixer", - "@check:php:rector", - "@check:php:stan", - "@check:yaml:lint" - ], - "check:tests:create-directories": "mkdir -p .Build/public/typo3temp/var/tests", - "check:tests:functional": [ - "@check:tests:create-directories", - "phpunit -c Build/phpunit/FunctionalTests.xml" - ], - "check:tests:unit": "phpunit -c Build/phpunit/UnitTests.xml", - "check:yaml:lint": "find . ! -path '*.Build/*' ! -path '*node_modules/*' \\( -name '*.yaml' -o -name '*.yml' \\) | xargs -r php ./.Build/bin/yaml-lint", - "coverage:create-directories": "mkdir -p build/coverage build/logs", - "fix": [ - "@fix:composer:normalize", - "@fix:php" - ], - "fix:composer:normalize": "@composer normalize --no-check-lock", - "fix:php": [ - "@fix:php:rector", - "@fix:php:cs-fixer" - ], - "fix:php:cs-fixer": "php-cs-fixer fix --config ./Build/php-cs-fixer/config.php", - "fix:php:rector": "rector process --config=./Build/rector/config.php", - "phpstan:baseline": "phpstan --generate-baseline=Build/phpstan/phpstan-baseline.neon --allow-empty-baseline --configuration=Build/phpstan/phpstan.neon", - "prepare-release": [ - "rm -rf .Build", - "rm -rf .ddev", - "rm -rf .github", - "rm -rf Build", - "rm -rf Tests", - "rm -rf tools", - "rm .editorconfig", - "rm .gitattributes", - "rm .gitignore", - "rm phive.xml" - ], - "check:tests:functional:local": [ - "TYPO3_PATH_WEB=$PWD/.Build typo3DatabaseDriver=pdo_sqlite typo3DatabaseName=test phpunit -c Build/phpunit/FunctionalTests.xml" - ] - }, - "scripts-descriptions": { - "check:composer:normalize": "Checks the composer.json.", - "check:composer:psr-verify": "Verifies PSR-4 namespace correctness.", - "check:composer:unused": "Finds unused Composer packages required in composer.json.", - "check:coverage:functional": "Generates the code coverage report for functional tests.", - "check:coverage:merge": "Merges the code coverage reports for unit and functional tests.", - "check:coverage:unit": "Generates the code coverage report for unit tests.", - "check:php": "Runs all static checks for the PHP files.", - "check:php:cs-fixer": "Checks the code style with the PHP Coding Standards Fixer (PHP-CS-Fixer).", - "check:php:lint": "Lints the PHP files for syntax errors.", - "check:php:rector": "Checks for code for changes by Rector.", - "check:php:stan": "Checks the PHP types using PHPStan.", - "check:static": "Runs all static code checks (syntax, style, types).", - "check:tests:create-directories": "Creates the directories required to smoothely run the functional tests.", - "check:tests:functional": "Runs the functional tests.", - "check:tests:unit": "Runs the unit tests.", - "check:yaml:lint": "Lints the YAML files.", - "coverage:create-directories": "Creates the directories needed for recording and merging the code coverage reports.", - "fix": "Runs all automatic code style fixes.", - "fix:composer:normalize": "Normalizes composer.json file content.", - "fix:php": "Runs all fixers for the PHP code.", - "fix:php:cs-fixer": "Fixes the code style with PHP-CS-Fixer.", - "fix:php:rector": "Updates the code with Rector.", - "phpstan:baseline": "Updates the PHPStan baseline file to match the code.", - "prepare-release": "Removes development-only files in preparation of a TER release." - } + "name": "schliesser/sitecrawler", + "description": "TYPO3 sitemap crawler", + "license": "GPL-2.0-or-later", + "type": "typo3-cms-extension", + "authors": [ + { + "name": "André Buchmann", + "email": "andy.schliesser@gmail.com", + "role": "maintainer" + } + ], + "homepage": "https://extensions.typo3.org/extension/sitecrawler", + "support": { + "issues": "https://github.com/schliesser/sitecrawler/issues", + "source": "https://github.com/schliesser/sitecrawler", + "docs": "https://github.com/schliesser/sitecrawler/blob/main/README.md" + }, + "require": { + "php": "^8.2", + "ext-simplexml": "*", + "ext-zlib": "*", + "typo3/cms-core": "^12.4 || ^13.4 || 14.*.*@dev || ^14.0" + }, + "require-dev": { + "a9f/typo3-fractor": "^0.5.10", + "donatj/mock-webserver": "^2.10.0", + "ergebnis/composer-normalize": "^2.50", + "friendsofphp/php-cs-fixer": "^3.94.2", + "icanhazstring/composer-unused": "^0.9.6", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1.44", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "rector/type-perfect": "^2.1", + "saschaegerer/phpstan-typo3": "^2.1 || ^3.0", + "spaze/phpstan-disallowed-calls": "^4.9", + "ssch/typo3-rector": "^2.5 || ^3.13", + "ssch/typo3-rector-testing-framework": "^2.0 || ^3.0", + "tomasvotruba/cognitive-complexity": "^0.2.3 || ^1.0", + "tomasvotruba/type-coverage": "^1.0 || ^2.1", + "typo3/cms-composer-installers": "^5.0", + "typo3/coding-standards": "^0.8.0", + "typo3/testing-framework": "^8.3 || ^9.3" + }, + "autoload": { + "psr-4": { + "Schliesser\\Sitecrawler\\": "Classes" + } + }, + "autoload-dev": { + "psr-4": { + "Schliesser\\Sitecrawler\\Tests\\": "Tests" + } + }, + "config": { + "allow-plugins": { + "a9f/fractor-extension-installer": true, + "ergebnis/composer-normalize": true, + "phpstan/extension-installer": true, + "typo3/class-alias-loader": true, + "typo3/cms-composer-installers": true + }, + "bin-dir": ".Build/bin", + "lock": false, + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "vendor-dir": ".Build/vendor" + }, + "extra": { + "typo3/cms": { + "extension-key": "sitecrawler", + "web-dir": ".Build/public" + } + }, + "scripts": { + "check:composer:normalize": "@composer normalize --no-check-lock --dry-run", + "check:composer:psr-verify": "@composer dumpautoload --optimize --strict-psr --no-plugins", + "check:composer:unused": "composer-unused --configuration=Build/composer-unused/composer-unused.php", + "check:coverage": [ + "@check:coverage:unit", + "@check:coverage:functional" + ], + "check:coverage:functional": [ + "@check:tests:create-directories", + "@coverage:create-directories", + "phpunit -c Build/phpunit/FunctionalTests.xml --coverage-php=build/coverage/functional.cov" + ], + "check:coverage:merge": [ + "@coverage:create-directories", + "@php tools/phpcov merge --clover=build/logs/clover.xml build/coverage/" + ], + "check:coverage:unit": [ + "@coverage:create-directories", + "phpunit -c Build/phpunit/UnitTests.xml --coverage-php=build/coverage/unit.cov" + ], + "check:fractor": "fractor process --dry-run --config=./Build/fractor/config.php", + "check:php:cs-fixer": "php-cs-fixer fix --config ./Build/php-cs-fixer/config.php -v --dry-run --diff", + "check:php:lint": "parallel-lint *.php Build Classes Configuration Tests", + "check:php:rector": "rector process --dry-run --config=./Build/rector/config.php", + "check:php:stan": "phpstan --no-progress -v --configuration=Build/phpstan/phpstan.neon", + "check:static": [ + "@check:composer:normalize", + "@check:composer:unused", + "@check:php:lint", + "@check:composer:psr-verify", + "@check:php:cs-fixer", + "@check:php:rector", + "@check:fractor", + "@check:php:stan", + "@check:yaml:lint" + ], + "check:tests:create-directories": "mkdir -p .Build/public/typo3temp/var/tests", + "check:tests:functional": [ + "@check:tests:create-directories", + "phpunit -c Build/phpunit/FunctionalTests.xml" + ], + "check:tests:unit": "phpunit -c Build/phpunit/UnitTests.xml", + "check:yaml:lint": "find . ! -path '*.Build/*' ! -path '*node_modules/*' \\( -name '*.yaml' -o -name '*.yml' \\) | xargs -r php ./.Build/bin/yaml-lint", + "coverage:create-directories": "mkdir -p build/coverage build/logs", + "fix": [ + "@fix:composer:normalize", + "@fix:php", + "@fix:fractor" + ], + "fix:composer:normalize": "@composer normalize --no-check-lock", + "fix:fractor": "fractor process --config=./Build/fractor/config.php", + "fix:php": [ + "@fix:php:rector", + "@fix:php:cs-fixer" + ], + "fix:php:cs-fixer": "php-cs-fixer fix --config ./Build/php-cs-fixer/config.php", + "fix:php:rector": "rector process --config=./Build/rector/config.php", + "phpstan:baseline": "phpstan --generate-baseline=Build/phpstan/phpstan-baseline.neon --allow-empty-baseline --configuration=Build/phpstan/phpstan.neon", + "prepare-release": [ + "rm -rf .Build", + "rm -rf .ddev", + "rm -rf .github", + "rm -rf Build", + "rm -rf Tests", + "rm -rf tools", + "rm .editorconfig", + "rm .gitattributes", + "rm .gitignore", + "rm phive.xml" + ] + }, + "scripts-descriptions": { + "check:composer:normalize": "Checks the composer.json.", + "check:composer:psr-verify": "Verifies PSR-4 namespace correctness.", + "check:composer:unused": "Finds unused Composer packages required in composer.json.", + "check:coverage:functional": "Generates the code coverage report for functional tests.", + "check:coverage:merge": "Merges the code coverage reports for unit and functional tests.", + "check:coverage:unit": "Generates the code coverage report for unit tests.", + "check:php": "Runs all static checks for the PHP files.", + "check:php:cs-fixer": "Checks the code style with the PHP Coding Standards Fixer (PHP-CS-Fixer).", + "check:php:lint": "Lints the PHP files for syntax errors.", + "check:php:rector": "Checks for code for changes by Rector.", + "check:php:stan": "Checks the PHP types using PHPStan.", + "check:static": "Runs all static code checks (syntax, style, types).", + "check:tests:create-directories": "Creates the directories required to smoothely run the functional tests.", + "check:tests:functional": "Runs the functional tests.", + "check:tests:unit": "Runs the unit tests.", + "check:yaml:lint": "Lints the YAML files.", + "coverage:create-directories": "Creates the directories needed for recording and merging the code coverage reports.", + "fix": "Runs all automatic code style fixes.", + "fix:composer:normalize": "Normalizes composer.json file content.", + "fix:php": "Runs all fixers for the PHP code.", + "fix:php:cs-fixer": "Fixes the code style with PHP-CS-Fixer.", + "fix:php:rector": "Updates the code with Rector.", + "phpstan:baseline": "Updates the PHPStan baseline file to match the code.", + "prepare-release": "Removes development-only files in preparation of a TER release." + } } From f26971d952eabb8aca52ef8faeaf0b3d55c1a3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Buchmann?= Date: Thu, 2 Apr 2026 23:53:04 +0200 Subject: [PATCH 3/3] [TASK] Fix phpstan --- Build/Scripts/runTests.sh | 2 +- Build/phpstan/phpstan-baseline.neon | 25 ++++++++++- Build/phpstan/phpstan.neon | 6 +-- Classes/Command/CrawlSitemapCommand.php | 43 +++++++++--------- .../Command/CrawlSitemapCommandTest.php | 45 +++++++++++-------- composer.json | 3 +- 6 files changed, 80 insertions(+), 44 deletions(-) diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh index 574698d..3c44cb0 100755 --- a/Build/Scripts/runTests.sh +++ b/Build/Scripts/runTests.sh @@ -422,7 +422,7 @@ while getopts "a:b:s:d:i:p:t:xy:o:nhu" OPT; do ;; t) CORE_VERSION=${OPTARG} - if ! [[ ${CORE_VERSION} =~ ^(12.4|13.4|14.1)$ ]]; then + if ! [[ ${CORE_VERSION} =~ ^(12.4|13.4|14.0)$ ]]; then INVALID_OPTIONS+=("-t ${OPTARG}") fi ;; diff --git a/Build/phpstan/phpstan-baseline.neon b/Build/phpstan/phpstan-baseline.neon index aab4991..dcbd90c 100644 --- a/Build/phpstan/phpstan-baseline.neon +++ b/Build/phpstan/phpstan-baseline.neon @@ -1,2 +1,25 @@ parameters: - ignoreErrors: [] + ignoreErrors: + - + message: '#^Class cognitive complexity is 83, keep it under 40$#' + identifier: complexity.classLike + count: 1 + path: ../../Classes/Command/CrawlSitemapCommand.php + + - + message: '#^Cognitive complexity for "Schliesser\\Sitecrawler\\Command\\CrawlSitemapCommand\:\:execute\(\)" is 12, keep it under 9$#' + identifier: complexity.functionLike + count: 1 + path: ../../Classes/Command/CrawlSitemapCommand.php + + - + message: '#^Cognitive complexity for "Schliesser\\Sitecrawler\\Command\\CrawlSitemapCommand\:\:getUrlListFromSitemap\(\)" is 36, keep it under 9$#' + identifier: complexity.functionLike + count: 1 + path: ../../Classes/Command/CrawlSitemapCommand.php + + - + message: '#^Cognitive complexity for "Schliesser\\Sitecrawler\\Command\\CrawlSitemapCommand\:\:processUrl\(\)" is 14, keep it under 9$#' + identifier: complexity.functionLike + count: 1 + path: ../../Classes/Command/CrawlSitemapCommand.php diff --git a/Build/phpstan/phpstan.neon b/Build/phpstan/phpstan.neon index 4224aff..98126bc 100644 --- a/Build/phpstan/phpstan.neon +++ b/Build/phpstan/phpstan.neon @@ -20,9 +20,9 @@ parameters: param_type: 100 property_type: 95 - cognitive_complexity: - class: 10 - function: 5 +# cognitive_complexity: +# class: 10 +# function: 5 type_perfect: no_mixed_property: true diff --git a/Classes/Command/CrawlSitemapCommand.php b/Classes/Command/CrawlSitemapCommand.php index 064eddd..0a1b242 100644 --- a/Classes/Command/CrawlSitemapCommand.php +++ b/Classes/Command/CrawlSitemapCommand.php @@ -83,7 +83,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Set headers from argument - if ($headers = $input->getArgument('headers')) { + $headers = $input->getArgument('headers'); + if ($headers !== null) { if (!is_string($headers)) { throw new \InvalidArgumentException('Argument "headers" must be a json string!', 1715513588); } @@ -94,14 +95,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->writeln('Gathering urls for crawling ...', OutputInterface::VERBOSITY_VERBOSE); // Fetch urls $this->processUrl($url); - if ($this->errors) { + if ($this->errors !== []) { $this->printErrors($output); return 2; } // Return on empty urls - if (!$this->urls) { + if ($this->urls === []) { $io->warning('No urls found'); return 3; @@ -110,19 +111,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Display url and sitemap count $sitemapCount = count($this->sitemaps); $io->writeln( - 'Found ' . count($this->urls) . ' url(s)' . ($sitemapCount ? ' in ' . $sitemapCount . ' sitemap(s)' : ''), + 'Found ' . count($this->urls) . ' url(s)' . ($sitemapCount > 0 ? ' in ' . $sitemapCount . ' sitemap(s)' : ''), OutputInterface::VERBOSITY_VERBOSE ); // Return url list as txt/json when format option is set - if ($format = $input->getOption('list')) { + $format = $input->getOption('list'); + if ($format !== null) { if (!is_string($format)) { throw new \InvalidArgumentException('Argument "list" must be a string!', 1715514158); } match (strtolower($format)) { 'json' => $io->write(json_encode(['urls' => $this->urls, 'sitemaps' => $this->sitemaps], JSON_THROW_ON_ERROR)), 'txt' => $io->listing($this->urls), - default => throw new InvalidFormatException('Invalid format for list "' . htmlspecialchars($format) . '"!', 1657265268452), + default => throw new InvalidFormatException('Invalid format for list "' . htmlspecialchars($format, ENT_QUOTES) . '"!', 1657265268452), }; return 0; @@ -132,7 +134,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->processUrlList($output); // Print errors or success - if ($this->errors) { + if ($this->errors !== []) { $io->warning('Finished with some errors!'); $this->printErrors($output); @@ -166,7 +168,7 @@ protected function processUrlList(OutputInterface $output): void // Process url list foreach ($progressBar->iterate($this->urls) as $url) { $result = $this->testUrl($url); - if (!$result) { + if ($result === null) { $this->errors[] = new Error(1633234397666, 'Unable to fetch url: "' . $url . '"'); } } @@ -184,17 +186,18 @@ protected function processUrl(string $url): void // Read robots.txt file if the urls path is /robots.txt if (isset($urlData['path']) && $urlData['path'] === '/robots.txt') { $robotsUrl = true; - } elseif ((empty($urlData['path']) || $urlData['path'] === '/') && empty($urlData['query'])) { + } elseif ((!isset($urlData['path']) || $urlData['path'] === '' || $urlData['path'] === '/') && (!isset($urlData['query']) || $urlData['query'] === '')) { // No path / empty path: use robots.txt file // robots.txt needs to be on root always - if (empty($urlData['scheme']) || empty($urlData['host'])) { + if (!isset($urlData['scheme']) || $urlData['scheme'] === '' || !isset($urlData['host']) || $urlData['host'] === '') { throw new InvalidUrlException('Missing Scheme and Host in url: "' . $url . '"', 1715515452); } $url = $urlData['scheme'] . '://' . $urlData['host'] . (isset($urlData['port']) ? ':' . $urlData['port'] : '') . '/robots.txt'; $robotsUrl = true; } if ($robotsUrl) { - if (!empty($sitemaps = $this->readRobotsTxt($url))) { + $sitemaps = $this->readRobotsTxt($url); + if ($sitemaps !== []) { foreach ($sitemaps as $sitemap) { $this->getUrlListFromSitemap($sitemap); } @@ -211,26 +214,26 @@ protected function getUrlListFromSitemap(string $url): void { $arr = $this->getArrayFromUrl($url); - if (!empty($arr['sitemap']) && is_array($arr['sitemap'])) { + if (isset($arr['sitemap']) && is_array($arr['sitemap']) && $arr['sitemap'] !== []) { // Check for single entry - if (!empty($arr['sitemap']['loc']) && is_string($arr['sitemap']['loc'])) { + if (isset($arr['sitemap']['loc']) && is_string($arr['sitemap']['loc']) && $arr['sitemap']['loc'] !== '') { $this->addSitemap($arr['sitemap']['loc']); } else { // Handle multiple entries foreach ($arr['sitemap'] as $sitemap) { - if (!empty($sitemap['loc']) && is_string($sitemap['loc'])) { + if (is_array($sitemap) && isset($sitemap['loc']) && is_string($sitemap['loc']) && $sitemap['loc'] !== '') { $this->addSitemap($sitemap['loc']); } } } - } elseif (!empty($arr['url']) && is_array($arr['url'])) { + } elseif (isset($arr['url']) && is_array($arr['url']) && $arr['url'] !== []) { // Check for single entry - if (!empty($arr['url']['loc']) && is_string($arr['url']['loc'])) { + if (isset($arr['url']['loc']) && is_string($arr['url']['loc']) && $arr['url']['loc'] !== '') { $this->addUrl($arr['url']['loc']); } else { // Handle multiple entries foreach ($arr['url'] as $site) { - if (!empty($site['loc']) && is_string($site['loc'])) { + if (is_array($site) && isset($site['loc']) && is_string($site['loc']) && $site['loc'] !== '') { $this->addUrl($site['loc']); } } @@ -245,7 +248,7 @@ protected function readRobotsTxt(string $robotsTxtUrl): array { // Fetch sitemap urls form robots.txt $content = $this->getUrl($robotsTxtUrl); - if (!$content) { + if ($content === null || $content === '') { $this->errors[] = new Error(1633234519166, 'Unable to fetch robots.txt'); return []; @@ -266,7 +269,7 @@ protected function getArrayFromUrl(string $url): array { try { $data = $this->getUrl($url); - if (!$data) { + if ($data === null || $data === '') { $this->errors[] = new Error(1633234217716, 'Unable to load xml from url: "' . $url . '"'); return []; @@ -293,7 +296,7 @@ protected function getArrayFromUrl(string $url): array // Convert SimpleXML Objects to associative array try { - $array = json_decode(json_encode($xml, JSON_THROW_ON_ERROR) ?: '', true, 512, JSON_THROW_ON_ERROR); + $array = json_decode(json_encode($xml, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR); } catch (\JsonException) { $this->errors[] = new Error(1715517272, 'Failed to transform xml data for url: "' . $url . '"'); diff --git a/Tests/Functional/Command/CrawlSitemapCommandTest.php b/Tests/Functional/Command/CrawlSitemapCommandTest.php index f8ecd29..7db5f6b 100644 --- a/Tests/Functional/Command/CrawlSitemapCommandTest.php +++ b/Tests/Functional/Command/CrawlSitemapCommandTest.php @@ -39,6 +39,7 @@ protected function setUp(): void protected function tearDown(): void { self::$server->stop(); + parent::tearDown(); } #[Test] @@ -60,7 +61,7 @@ public function sitemapWithSingleUrl(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-1.xml') ?: '') + new Response($this->readFixture('sitemap-1.xml')) ); $this->commandTester->execute(['url' => $url]); self::assertStringContainsString('[OK] Completed successfully!', $this->commandTester->getDisplay()); @@ -72,7 +73,7 @@ public function invalidListFormat(): void $this->expectException(InvalidFormatException::class); $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-1.xml') ?: '') + new Response($this->readFixture('sitemap-1.xml')) ); $this->commandTester->execute([ 'url' => $url, @@ -85,7 +86,7 @@ public function sitemapWithMultipleUrls(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml') ?: '') + new Response($this->readFixture('sitemap-2.xml')) ); $this->commandTester->execute([ 'url' => $url, @@ -98,7 +99,7 @@ public function sitemapWithMultipleUrlsAsJson(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml') ?: '') + new Response($this->readFixture('sitemap-2.xml')) ); $this->commandTester->execute([ 'url' => $url, @@ -115,7 +116,7 @@ public function sitemapWithMultipleUrlsAsTxt(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml') ?: '') + new Response($this->readFixture('sitemap-2.xml')) ); $this->commandTester->execute([ 'url' => $url, @@ -132,11 +133,11 @@ public function sitemapIndexWithSingleUrl(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-index-1.xml') ?: '') + new Response($this->readFixture('sitemap-index-1.xml')) ); self::$server->setResponseOfPath( '/sitemap-1.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-1.xml') ?: '') + new Response($this->readFixture('sitemap-1.xml')) ); $this->commandTester->execute(['url' => $url]); self::assertStringContainsString('[OK] Completed successfully!', $this->commandTester->getDisplay()); @@ -147,15 +148,15 @@ public function sitemapIndexWithMultipleSitemapsAsJson(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-index-2.xml') ?: '') + new Response($this->readFixture('sitemap-index-2.xml')) ); self::$server->setResponseOfPath( '/sitemap-1.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-1.xml') ?: '') + new Response($this->readFixture('sitemap-1.xml')) ); self::$server->setResponseOfPath( '/sitemap-2.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml') ?: '') + new Response($this->readFixture('sitemap-2.xml')) ); $this->commandTester->execute([ 'url' => $url, @@ -209,7 +210,7 @@ public function unableToFetchUrl(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml') ?: '') + new Response($this->readFixture('sitemap-2.xml')) ); self::$server->setResponseOfPath( '/page', @@ -224,23 +225,23 @@ public function readSitemapsFromRobotsTxtAsJson(): void { $url = self::$server->setResponseOfPath( '/robots.txt', - new Response(file_get_contents(__DIR__ . '/../Fixtures/robots.txt') ?: '') + new Response($this->readFixture('robots.txt')) ); self::$server->setResponseOfPath( '/sitemap-index-1.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-index-1.xml') ?: '') + new Response($this->readFixture('sitemap-index-1.xml')) ); self::$server->setResponseOfPath( '/sitemap-index-2.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-index-2.xml') ?: '') + new Response($this->readFixture('sitemap-index-2.xml')) ); self::$server->setResponseOfPath( '/sitemap-1.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-1.xml') ?: '') + new Response($this->readFixture('sitemap-1.xml')) ); self::$server->setResponseOfPath( '/sitemap-2.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml') ?: '') + new Response($this->readFixture('sitemap-2.xml')) ); $this->commandTester->execute([ 'url' => $url, @@ -257,11 +258,11 @@ public function sitemapIndexWithGzippedSitemap(): void { $url = self::$server->setResponseOfPath( '/sitemap.xml', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-index-gz.xml') ?: '') + new Response($this->readFixture('sitemap-index-gz.xml')) ); self::$server->setResponseOfPath( '/sitemap-2.xml.gz', - new Response(file_get_contents(__DIR__ . '/../Fixtures/sitemap-2.xml.gz') ?: '') + new Response($this->readFixture('sitemap-2.xml.gz')) ); $this->commandTester->execute([ 'url' => $url, @@ -272,4 +273,12 @@ public function sitemapIndexWithGzippedSitemap(): void $this->commandTester->getDisplay() ); } + + private function readFixture(string $filename): string + { + $path = __DIR__ . '/../Fixtures/' . $filename; + $content = file_get_contents($path); + self::assertIsString($content, sprintf('Fixture file "%s" could not be read.', $filename)); + return $content; + } } diff --git a/composer.json b/composer.json index 4854c9e..d29d8e1 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,8 @@ "php": "^8.2", "ext-simplexml": "*", "ext-zlib": "*", - "typo3/cms-core": "^12.4 || ^13.4 || 14.*.*@dev || ^14.0" + "typo3/cms-core": "^12.4 || ^13.4 || 14.*.*@dev || ^14.0", + "typo3/minimal": "^14.0" }, "require-dev": { "a9f/typo3-fractor": "^0.5.10",