From b67ad2b1038e43fba7e3e5a6b7e5b16ef19a9f5b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:13:09 +0200 Subject: [PATCH 001/107] try running on other operating systems --- .github/workflows/testing.yml | 347 +++++++++++++++++++++++++++++++++- 1 file changed, 345 insertions(+), 2 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index bf67592d8..f00b83903 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -10,6 +10,349 @@ on: schedule: - cron: '17 1 * * *' # Run every day on a seemly random time. +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: - test: - uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main + get-matrix: + name: Get base test matrix + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.base-matrix.outputs.matrix }} + steps: + - name: Set matrix + id: base-matrix + run: | + MATRIX=$(cat << EOF + { + "include": [ + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite" + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0", + "os": "macos-15" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite", + "os": "macos-15" + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0", + "os": "windows-2025" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite", + "os": "windows-2025" + }, + ] + } + EOF + ) + echo matrix=$MATRIX >> $GITHUB_OUTPUT + + prepare-unit: + name: Prepare matrix for unit tests + needs: get-matrix + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Check out source code + uses: actions/checkout@v5 + + - name: Check existence of composer.json & phpunit.xml.dist files + id: check_files + uses: andstor/file-existence-action@v3 + with: + files: "composer.json, phpunit.xml.dist" + + - name: Set matrix + id: set-matrix + run: | + if [[ $FILE_EXISTS == 'true' ]]; then + echo "matrix=$(jq -c \ + --argjson with_coverage_flag "${{ inputs.with-coverage }}" \ + --arg minimum_php "${{ inputs.minimum-php }}" \ + --arg minimum_wp "${{ inputs.minimum-wp }}" \ + ' + .include |= ( + map( + # First, select only the versions that meet all minimum requirements + select( + (.php >= $minimum_php) and + (.wp == "latest" or .wp >= $minimum_wp) + ) | + + # Next, update the coverage flag on the remaining items + if $with_coverage_flag == false and .coverage == true then + .coverage = false + else + . + end + ) | + + # Finally, get the unique entries + unique_by(.php) + ) + ' <<< "$BASE_MATRIX")" >> $GITHUB_OUTPUT + else + echo "matrix=" >> $GITHUB_OUTPUT + fi + env: + BASE_MATRIX: ${{ needs.get-matrix.outputs.matrix }} + FILE_EXISTS: ${{ steps.check_files.outputs.files_exists == 'true' }} + + unit: #----------------------------------------------------------------------- + needs: prepare-unit + if: ${{ needs.prepare-unit.outputs.matrix != '' }} + name: Unit test / PHP ${{ matrix.php }}${{ matrix.coverage && ' (with coverage)' || '' }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-unit.outputs.matrix) }} + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} + + continue-on-error: ${{ matrix.php == 'nightly' }} + + steps: + - name: Check out source code + uses: actions/checkout@v5 + + - name: Set up PHP environment + uses: shivammathur/setup-php@v2 + with: + php-version: '${{ matrix.php }}' + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} + tools: composer,cs2pr + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Composer dependencies & cache dependencies + uses: "ramsey/composer-install@v3" + env: + COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Grab PHPUnit version + id: phpunit_version + run: echo "VERSION=$(vendor/bin/phpunit --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT + + # PHPUnit 10+ may fail a test run when the "old" configuration format is used. + # Luckily, there is a build-in migration tool since PHPUnit 9.3. + - name: Migrate PHPUnit configuration for PHPUnit 10+ + if: ${{ startsWith( steps.phpunit_version.outputs.VERSION, '1' ) }} + continue-on-error: true + run: composer phpunit -- --migrate-configuration + + - name: Setup problem matcher to provide annotations for PHPUnit + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + + - name: Run PHPUnit + run: | + if [[ ${{ matrix.coverage == true }} == true ]]; then + composer phpunit -- --coverage-clover build/logs/unit-coverage.xml + else + composer phpunit + fi + + - name: Upload code coverage report + if: ${{ matrix.coverage }} + uses: codecov/codecov-action@v5.5.1 + with: + directory: build/logs + flags: unit + token: ${{ secrets.CODECOV_TOKEN }} + + prepare-functional: #--------------------------------------------------------- + name: Prepare matrix for functional tests + needs: get-matrix + runs-on: ubuntu-22.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Check out source code + uses: actions/checkout@v5 + + - name: Check existence of composer.json & behat.yml files + id: check_files + uses: andstor/file-existence-action@v3 + with: + files: "composer.json, behat.yml" + + - name: Set matrix + id: set-matrix + run: | + if [[ $FILE_EXISTS == 'true' ]]; then + echo "matrix=$(jq -c \ + --argjson with_coverage_flag "${{ inputs.with-coverage }}" \ + --arg minimum_php "${{ inputs.minimum-php }}" \ + --arg minimum_wp "${{ inputs.minimum-wp }}" \ + ' + # First, select only the versions that meet all minimum requirements + .include |= ( + map( + select( + .php >= $minimum_php + ) | + # Next, update the coverage flag on the remaining items + if $with_coverage_flag == false and .coverage == true then + .coverage = false + else + . + end + ) + ) | + + # Reassign WP4.9 to minimum_wp + .include |= ( + map( + select( + .wp == "4.9" + ).wp |= $minimum_wp + ) + ) + ' <<< "$BASE_MATRIX" )" >> $GITHUB_OUTPUT + else + echo "matrix=" >> $GITHUB_OUTPUT + fi + env: + BASE_MATRIX: ${{ needs.get-matrix.outputs.matrix }} + FILE_EXISTS: ${{ steps.check_files.outputs.files_exists == 'true' }} + + functional: #----------------------------------------------------------------- + needs: prepare-functional + if: ${{ needs.prepare-functional.outputs.matrix != '' }} + name: Functional - WP ${{ matrix.wp }} on PHP ${{ matrix.php }} with ${{ matrix.dbtype != 'sqlite' && matrix.mysql || 'SQLite' }}${{ matrix.coverage && ' (with coverage)' || '' }} + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.prepare-functional.outputs.matrix) }} + runs-on: ubuntu-22.04 + + continue-on-error: ${{ matrix.dbtype == 'sqlite' || matrix.dbtype == 'mariadb' || matrix.php == 'nightly' }} + + steps: + - name: Check out source code + uses: actions/checkout@v5 + + - name: Install Ghostscript + run: | + sudo apt-get update + sudo apt-get install ghostscript -y + + - name: Set up PHP environment + uses: shivammathur/setup-php@v2 + with: + php-version: '${{ matrix.php }}' + ini-values: zend.assertions=1, error_reporting=-1, display_errors=On + extensions: gd, imagick, mysql, zip + coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} + tools: composer + env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Change ImageMagick policy to allow pdf->png conversion. + run: | + sudo sed -i 's/^.*policy.*coder.*none.*PDF.*//' /etc/ImageMagick-6/policy.xml + + - name: Install Composer dependencies & cache dependencies + uses: "ramsey/composer-install@v3" + env: + COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Setup MySQL Server + id: setup-mysql + if: ${{ matrix.dbtype != 'sqlite' }} + uses: shogo82148/actions-setup-mysql@v1 + with: + mysql-version: ${{ matrix.mysql }} + auto-start: true + root-password: root + user: wp_cli_test + password: password1 + my-cnf: | + default_authentication_plugin=mysql_native_password + + - name: Configure DB environment + if: ${{ matrix.dbtype != 'sqlite' }} + run: | + echo "MYSQL_HOST=127.0.0.1" >> $GITHUB_ENV + echo "MYSQL_TCP_PORT=3306" >> $GITHUB_ENV + echo "WP_CLI_TEST_DBROOTUSER=root" >> $GITHUB_ENV + echo "WP_CLI_TEST_DBROOTPASS=root" >> $GITHUB_ENV + echo "WP_CLI_TEST_DBNAME=wp_cli_test" >> $GITHUB_ENV + echo "WP_CLI_TEST_DBUSER=wp_cli_test" >> $GITHUB_ENV + echo "WP_CLI_TEST_DBPASS=password1" >> $GITHUB_ENV + echo "WP_CLI_TEST_DBHOST=127.0.0.1:3306" >> $GITHUB_ENV + + - name: Prepare test database + if: ${{ matrix.dbtype != 'sqlite' }} + run: composer prepare-tests + + - name: Check Behat environment + env: + WP_VERSION: '${{ matrix.wp }}' + WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} + WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' + run: WP_CLI_TEST_DEBUG_BEHAT_ENV=1 composer behat + + - name: Run Behat + env: + WP_VERSION: '${{ matrix.wp }}' + WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} + WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' + WP_CLI_TEST_COVERAGE: ${{ matrix.coverage }} + run: | + ARGS=() + + if [[ $WP_CLI_TEST_COVERAGE == 'true' ]]; then + # The flag was only added in v3.17.0 + if composer behat -- --help 2>/dev/null | grep xdebug; then + ARGS+=("--xdebug") + fi + fi + + if [[ $RUNNER_DEBUG == '1' ]]; then + ARGS+=("--format=pretty") + fi + + composer behat -- "${ARGS[@]}" || composer behat-rerun -- "${ARGS[@]}" + + - name: Retrieve list of coverage files + id: coverage_files + if: ${{ matrix.coverage }} + run: | + FILES=$(find "$GITHUB_WORKSPACE/build/logs" -path '*.*' | paste -s -d "," -) + echo "files=$FILES" >> $GITHUB_OUTPUT + + - name: Upload code coverage report + if: ${{ matrix.coverage }} + uses: codecov/codecov-action@v5.5.1 + with: + # Because somehow providing `directory: build/logs` doesn't work for these files + files: ${{ steps.coverage_files.outputs.files }} + flags: feature + token: ${{ secrets.CODECOV_TOKEN }} From ae12662234ed850ca8c81651cf0f252903f5a743 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:23:38 +0200 Subject: [PATCH 002/107] fixes --- .github/workflows/testing.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f00b83903..a01e6ad99 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -62,7 +62,7 @@ jobs: "wp": "latest", "dbtype": "sqlite", "os": "windows-2025" - }, + } ] } EOF @@ -90,9 +90,9 @@ jobs: run: | if [[ $FILE_EXISTS == 'true' ]]; then echo "matrix=$(jq -c \ - --argjson with_coverage_flag "${{ inputs.with-coverage }}" \ - --arg minimum_php "${{ inputs.minimum-php }}" \ - --arg minimum_wp "${{ inputs.minimum-wp }}" \ + --argjson with_coverage_flag "true" \ + --arg minimum_php "7.2" \ + --arg minimum_wp "4.9" \ ' .include |= ( map( From 761156fde3c0a81fa5b1b7d32435d2af90d9a4a1 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:26:21 +0200 Subject: [PATCH 003/107] fixes --- .github/workflows/testing.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a01e6ad99..a4459e4c6 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -205,9 +205,9 @@ jobs: run: | if [[ $FILE_EXISTS == 'true' ]]; then echo "matrix=$(jq -c \ - --argjson with_coverage_flag "${{ inputs.with-coverage }}" \ - --arg minimum_php "${{ inputs.minimum-php }}" \ - --arg minimum_wp "${{ inputs.minimum-wp }}" \ + --argjson with_coverage_flag "true" \ + --arg minimum_php "7.2" \ + --arg minimum_wp "4.9" \ ' # First, select only the versions that meet all minimum requirements .include |= ( From d8d8eb5854fa7a5e70708f2c0a0251e81e1c51cc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:30:44 +0200 Subject: [PATCH 004/107] change title --- .github/workflows/testing.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index a4459e4c6..4df5c088d 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -124,7 +124,7 @@ jobs: unit: #----------------------------------------------------------------------- needs: prepare-unit if: ${{ needs.prepare-unit.outputs.matrix != '' }} - name: Unit test / PHP ${{ matrix.php }}${{ matrix.coverage && ' (with coverage)' || '' }} + name: Unit test / PHP ${{ matrix.php }}${{ matrix.coverage && ' (with coverage)' || '' }} (${{ matrix.os || 'ubuntu-22.04' }}) strategy: fail-fast: false matrix: ${{ fromJson(needs.prepare-unit.outputs.matrix) }} @@ -243,11 +243,11 @@ jobs: functional: #----------------------------------------------------------------- needs: prepare-functional if: ${{ needs.prepare-functional.outputs.matrix != '' }} - name: Functional - WP ${{ matrix.wp }} on PHP ${{ matrix.php }} with ${{ matrix.dbtype != 'sqlite' && matrix.mysql || 'SQLite' }}${{ matrix.coverage && ' (with coverage)' || '' }} + name: Functional - WP ${{ matrix.wp }} on PHP ${{ matrix.php }} with ${{ matrix.dbtype != 'sqlite' && matrix.mysql || 'SQLite' }}${{ matrix.coverage && ' (with coverage)' || '' }} (${{ matrix.os || 'ubuntu-22.04' }}) strategy: fail-fast: false matrix: ${{ fromJson(needs.prepare-functional.outputs.matrix) }} - runs-on: ubuntu-22.04 + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} continue-on-error: ${{ matrix.dbtype == 'sqlite' || matrix.dbtype == 'mariadb' || matrix.php == 'nightly' }} From a6c290068957d66c7a3c7eb5c44fa7d4e8bda522 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:40:39 +0200 Subject: [PATCH 005/107] unique by os --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4df5c088d..c15471c16 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -111,7 +111,7 @@ jobs: ) | # Finally, get the unique entries - unique_by(.php) + unique_by([.php, .os]) ) ' <<< "$BASE_MATRIX")" >> $GITHUB_OUTPUT else From ffcdfa190f0e7870a3e50532eaf309bbcb6b7793 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:40:44 +0200 Subject: [PATCH 006/107] conditional apt-get --- .github/workflows/testing.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c15471c16..c31f3447a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -256,6 +256,7 @@ jobs: uses: actions/checkout@v5 - name: Install Ghostscript + if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == '' }} run: | sudo apt-get update sudo apt-get install ghostscript -y From 8fef07e5c37dc29b91feedcca9ad16c6fa5a2a84 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:56:06 +0200 Subject: [PATCH 007/107] more fixes --- .github/workflows/testing.yml | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index c31f3447a..04f57d2e0 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -30,35 +30,35 @@ jobs: { "include": [ { - "php": "8.4", + "php": 8.4, "wp": "latest", "mysql": "mysql-8.0" }, { - "php": "8.4", + "php": 8.4, "wp": "latest", "dbtype": "sqlite" }, { - "php": "8.4", + "php": 8.4, "wp": "latest", "mysql": "mysql-8.0", "os": "macos-15" }, { - "php": "8.4", + "php": 8.4, "wp": "latest", "dbtype": "sqlite", "os": "macos-15" }, { - "php": "8.4", + "php": 8.4, "wp": "latest", "mysql": "mysql-8.0", "os": "windows-2025" }, { - "php": "8.4", + "php": 8.4, "wp": "latest", "dbtype": "sqlite", "os": "windows-2025" @@ -154,27 +154,25 @@ jobs: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") - - name: Grab PHPUnit version - id: phpunit_version - run: echo "VERSION=$(vendor/bin/phpunit --version | grep --only-matching --max-count=1 --extended-regexp '\b[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT - # PHPUnit 10+ may fail a test run when the "old" configuration format is used. # Luckily, there is a build-in migration tool since PHPUnit 9.3. - name: Migrate PHPUnit configuration for PHPUnit 10+ - if: ${{ startsWith( steps.phpunit_version.outputs.VERSION, '1' ) }} + if: ${{ matrix.php >= 8.2 || matrix.php == 'nightly' }} continue-on-error: true run: composer phpunit -- --migrate-configuration - name: Setup problem matcher to provide annotations for PHPUnit run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Run PHPUnit with coverage + if: ${{ matrix.coverage }} + run: | + composer phpunit -- --coverage-clover build/logs/unit-coverage.xml + - name: Run PHPUnit + if: ${{ ! matrix.coverage }} run: | - if [[ ${{ matrix.coverage == true }} == true ]]; then - composer phpunit -- --coverage-clover build/logs/unit-coverage.xml - else - composer phpunit - fi + composer phpunit - name: Upload code coverage report if: ${{ matrix.coverage }} @@ -273,6 +271,7 @@ jobs: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Change ImageMagick policy to allow pdf->png conversion. + if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == '' }} run: | sudo sed -i 's/^.*policy.*coder.*none.*PDF.*//' /etc/ImageMagick-6/policy.xml From 3ba9f228ad9ea3cf646679c5c45dd577511278de Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 23:09:18 +0200 Subject: [PATCH 008/107] undo string change --- .github/workflows/testing.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 04f57d2e0..4f708470e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -30,35 +30,35 @@ jobs: { "include": [ { - "php": 8.4, + "php": "8.4", "wp": "latest", "mysql": "mysql-8.0" }, { - "php": 8.4, + "php": "8.4", "wp": "latest", "dbtype": "sqlite" }, { - "php": 8.4, + "php": "8.4", "wp": "latest", "mysql": "mysql-8.0", "os": "macos-15" }, { - "php": 8.4, + "php": "8.4", "wp": "latest", "dbtype": "sqlite", "os": "macos-15" }, { - "php": 8.4, + "php": "8.4", "wp": "latest", "mysql": "mysql-8.0", "os": "windows-2025" }, { - "php": 8.4, + "php": "8.4", "wp": "latest", "dbtype": "sqlite", "os": "windows-2025" From 981a8eaad53ca84f385a05e183ecf26e0ab8aa8c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 23:21:06 +0200 Subject: [PATCH 009/107] Get db version only when needed --- utils/behat-tags.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/utils/behat-tags.php b/utils/behat-tags.php index c6fa9833e..4a8bad233 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -85,11 +85,11 @@ function get_db_version() { $skip_tags[] = '@broken-trunk'; } -$db_version = get_db_version(); switch ( getenv( 'WP_CLI_TEST_DBTYPE' ) ) { case 'mariadb': - $skip_tags = array_merge( + $db_version = get_db_version(); + $skip_tags = array_merge( $skip_tags, [ '@require-mysql', '@require-sqlite' ], version_tags( 'require-mariadb', $db_version, '<', $features_folder ), @@ -103,7 +103,8 @@ function get_db_version() { break; case 'mysql': default: - $skip_tags = array_merge( + $db_version = get_db_version(); + $skip_tags = array_merge( $skip_tags, [ '@require-mariadb', '@require-sqlite' ], version_tags( 'require-mysql', $db_version, '<', $features_folder ), From d1e8aea1845b066fa0fe75c1173e3ef532afb68b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 23:25:18 +0200 Subject: [PATCH 010/107] set env var like the others --- .github/workflows/testing.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 4f708470e..6afa128b5 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -317,7 +317,8 @@ jobs: WP_VERSION: '${{ matrix.wp }}' WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' - run: WP_CLI_TEST_DEBUG_BEHAT_ENV=1 composer behat + WP_CLI_TEST_DEBUG_BEHAT_ENV: 1 + run: composer behat - name: Run Behat env: From 035a6551b813070637ea40d89dd99e9750ec9db8 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 23:31:59 +0200 Subject: [PATCH 011/107] use string instead of array --- .github/workflows/testing.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 6afa128b5..07c1fffab 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -327,20 +327,20 @@ jobs: WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' WP_CLI_TEST_COVERAGE: ${{ matrix.coverage }} run: | - ARGS=() + ARGS="" if [[ $WP_CLI_TEST_COVERAGE == 'true' ]]; then # The flag was only added in v3.17.0 if composer behat -- --help 2>/dev/null | grep xdebug; then - ARGS+=("--xdebug") + ARGS+=" --xdebug" fi fi if [[ $RUNNER_DEBUG == '1' ]]; then - ARGS+=("--format=pretty") + ARGS+=" --format=pretty" fi - composer behat -- "${ARGS[@]}" || composer behat-rerun -- "${ARGS[@]}" + composer behat -- $ARGS || composer behat-rerun -- $ARGS - name: Retrieve list of coverage files id: coverage_files From 2f6fdb8c20a2612ba8afa403a94fb452d5072060 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 08:22:57 +0200 Subject: [PATCH 012/107] pass behat args via step env --- .github/workflows/testing.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 07c1fffab..f37eec30b 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -326,21 +326,9 @@ jobs: WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' WP_CLI_TEST_COVERAGE: ${{ matrix.coverage }} + BEHAT_ARGS: ${{ matrix.coverage && env.RUNNER_DEBUG && '--debug --format=pretty' || matrix.coverage && '--debug' || env.RUNNER_DEBUG && '--format=pretty' || '' }} run: | - ARGS="" - - if [[ $WP_CLI_TEST_COVERAGE == 'true' ]]; then - # The flag was only added in v3.17.0 - if composer behat -- --help 2>/dev/null | grep xdebug; then - ARGS+=" --xdebug" - fi - fi - - if [[ $RUNNER_DEBUG == '1' ]]; then - ARGS+=" --format=pretty" - fi - - composer behat -- $ARGS || composer behat-rerun -- $ARGS + composer behat -- $BEHAT_ARGS || composer behat-rerun -- $BEHAT_ARGS - name: Retrieve list of coverage files id: coverage_files From cb7887f5ca8f7a8d4ab986958fe524d92bdb3eb7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 08:24:19 +0200 Subject: [PATCH 013/107] Set `WP_CLI_TEST_DBTYPE` for unit tests --- .github/workflows/testing.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index f37eec30b..7a8cb6e7a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -171,6 +171,9 @@ jobs: - name: Run PHPUnit if: ${{ ! matrix.coverage }} + # For example TestBehatTags.php in wp-cli-tests depends on the db type. + env: + WP_CLI_TEST_DBTYPE: 'sqlite' run: | composer phpunit From ac42de12569c6aa835b3b1ff6bb0fc438700bbd3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 08:45:19 +0200 Subject: [PATCH 014/107] No colors on CI --- bin/install-package-tests | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bin/install-package-tests b/bin/install-package-tests index 2646ebb1b..6821677de 100755 --- a/bin/install-package-tests +++ b/bin/install-package-tests @@ -15,11 +15,18 @@ is_numeric() { *) return 0;; # returns 0 if numeric esac } -# Promt color vars. +# Prompt color vars. C_RED="\033[31m" C_BLUE="\033[34m" NO_FORMAT="\033[0m" +# If running in CI, don't use colors. +if [ -n "${CI}" ]; then + C_RED="" + C_BLUE="" + NO_FORMAT="" +fi + HOST=localhost PORT="" HOST_STRING='' From 7dc567608af8f4e581bcb54ca497038439611761 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 08:49:05 +0200 Subject: [PATCH 015/107] Set db env vars at job level --- .github/workflows/testing.yml | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 7a8cb6e7a..838d71d81 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -252,6 +252,16 @@ jobs: continue-on-error: ${{ matrix.dbtype == 'sqlite' || matrix.dbtype == 'mariadb' || matrix.php == 'nightly' }} + env: + MYSQL_HOST: 127.0.0.1 + MYSQL_TCP_PORT: 3306 + WP_CLI_TEST_DBROOTUSER: root + WP_CLI_TEST_DBROOTPASS: root + WP_CLI_TEST_DBNAME: wp_cli_test + WP_CLI_TEST_DBUSER: wp_cli_test + WP_CLI_TEST_DBPASS: password1 + WP_CLI_TEST_DBHOST: 127.0.0.1:3306 + steps: - name: Check out source code uses: actions/checkout@v5 @@ -293,24 +303,12 @@ jobs: with: mysql-version: ${{ matrix.mysql }} auto-start: true - root-password: root - user: wp_cli_test - password: password1 + root-password: ${{ env.WP_CLI_TEST_DBROOTPASS }} + user: ${{ env.WP_CLI_TEST_DBUSER}} + password: ${{ env.WP_CLI_TEST_DBPASS}} my-cnf: | default_authentication_plugin=mysql_native_password - - name: Configure DB environment - if: ${{ matrix.dbtype != 'sqlite' }} - run: | - echo "MYSQL_HOST=127.0.0.1" >> $GITHUB_ENV - echo "MYSQL_TCP_PORT=3306" >> $GITHUB_ENV - echo "WP_CLI_TEST_DBROOTUSER=root" >> $GITHUB_ENV - echo "WP_CLI_TEST_DBROOTPASS=root" >> $GITHUB_ENV - echo "WP_CLI_TEST_DBNAME=wp_cli_test" >> $GITHUB_ENV - echo "WP_CLI_TEST_DBUSER=wp_cli_test" >> $GITHUB_ENV - echo "WP_CLI_TEST_DBPASS=password1" >> $GITHUB_ENV - echo "WP_CLI_TEST_DBHOST=127.0.0.1:3306" >> $GITHUB_ENV - - name: Prepare test database if: ${{ matrix.dbtype != 'sqlite' }} run: composer prepare-tests From e368b2612d6c6a12f869a968d7f9d76388b21370 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:32:22 +0200 Subject: [PATCH 016/107] Add output buffer to avoid unexpected output --- tests/tests/TestBehatTags.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 77e6b7679..3674902c0 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -198,7 +198,12 @@ public function test_behat_tags_db_version(): void { $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + + // Just to get the get_db_version() function. + ob_start(); require $behat_tags; + ob_end_clean(); + // @phpstan-ignore-next-line $db_version = get_db_version(); $minimum_db_version = $db_version . '.1'; From c20a32c0571709d70948a690de7bcb25bc52ddd2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:32:48 +0200 Subject: [PATCH 017/107] Add `WP_CLI_TEST_DBTYPE` conditional --- tests/tests/TestBehatTags.php | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 3674902c0..d5b4537aa 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -143,10 +143,30 @@ public function test_behat_tags_php_version(): void { $this->markTestSkipped( "No test for PHP_VERSION $php_version." ); } + $expected .= '&&~@github-api&&~@broken'; + + $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); + switch ( $db_type ) { + case 'mariadb': + $expected .= '&&~@require-mysql'; + $expected .= '&&~@require-sqlite'; + break; + case 'sqlite': + $expected .= '&&~@require-mariadb'; + $expected .= '&&~@require-mysql'; + $expected .= '&&~@require-mysql-or-mariadb'; + break; + case 'mysql': + default: + $expected .= '&&~@require-mariadb'; + $expected .= '&&~@require-sqlite'; + break; + } + file_put_contents( $this->temp_dir . '/features/php_version.feature', $contents ); $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); - $this->assertSame( '--tags=' . $expected . '&&~@github-api&&~@broken&&~@require-mariadb&&~@require-sqlite', $output ); + $this->assertSame( '--tags=' . $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); } From 04fd1a245ddac1a314a02c1bd1e0fa84f7ab8390 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:35:12 +0200 Subject: [PATCH 018/107] try realpath to see if it helps --- tests/tests/TestBehatTags.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index d5b4537aa..13ba9c7c0 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -177,7 +177,7 @@ public function test_behat_tags_extension(): void { putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + $behat_tags = realpath( dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php' ); file_put_contents( $this->temp_dir . '/features/extension.feature', '@require-extension-imagick @require-extension-curl' ); @@ -217,7 +217,7 @@ public function test_behat_tags_extension(): void { public function test_behat_tags_db_version(): void { $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + $behat_tags = realpath( dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php' ); // Just to get the get_db_version() function. ob_start(); From 8655f8a51157b2d30524491bfece22bbc40923d9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:45:13 +0200 Subject: [PATCH 019/107] Use `curl.exe` --- src/Context/FeatureContext.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 4ec3c15a3..929b0dc28 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -416,6 +416,15 @@ public static function get_bin_path(): ?string { return $bin_path; } + /** + * Whether the current OS is Windows. + * + * @return bool + */ + static private function is_windows(): bool { + return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + } + /** * Get the environment variables required for launched `wp` processes. * @@ -565,9 +574,11 @@ private static function download_sqlite_plugin( $dir ): void { mkdir( $dir ); } + $curl = self::is_windows() ? 'curl.exe' : 'curl'; + Process::create( Utils\esc_cmd( - 'curl -sSfL %1$s > %2$s', + "$curl -sSfL %1\$s > %2\$s", $download_url, $download_location ) @@ -1078,9 +1089,11 @@ public function download_phar( $version = 'same' ): void { . uniqid( 'wp-cli-download-', true ) . '.phar'; + $curl = self::is_windows() ? 'curl.exe' : 'curl'; + Process::create( Utils\esc_cmd( - 'curl -sSfL %1$s > %2$s && chmod +x %2$s', + "$curl -sSfL %1\$s > %2\$s && chmod +x %2\$s", $download_url, $this->variables['PHAR_PATH'] ) From fc0c67682b00a7f7bcad2046d9baf61cadb621b0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:45:18 +0200 Subject: [PATCH 020/107] Use `del` --- src/Context/FeatureContext.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 929b0dc28..fd739548f 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1273,7 +1273,11 @@ public function move_files( $src, $dest ): void { * @param string $dir */ public static function remove_dir( $dir ): void { - Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + if ( self::is_windows() ) { + Process::create( Utils\esc_cmd( 'del %s', $dir ) )->run_check(); + } else { + Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + } } /** From 63da4e89218f3cf3dac971333e6d939983e6a770 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:46:12 +0200 Subject: [PATCH 021/107] lint fix --- src/Context/FeatureContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index fd739548f..cad0c36bf 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -421,8 +421,8 @@ public static function get_bin_path(): ?string { * * @return bool */ - static private function is_windows(): bool { - return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN'; + private static function is_windows(): bool { + return strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; } /** From 98b15835fdfebefa9473b912ec5f403d3935538c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:49:30 +0200 Subject: [PATCH 022/107] Use `copy` on windows --- src/Context/FeatureContext.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index cad0c36bf..d580b45a9 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1287,9 +1287,12 @@ public static function remove_dir( $dir ): void { * @param string $dest_dir */ public static function copy_dir( $src_dir, $dest_dir ): void { - $shell_command = ( 'Darwin' === PHP_OS ) - ? Utils\esc_cmd( 'cp -R %s/* %s', $src_dir, $dest_dir ) - : Utils\esc_cmd( 'cp -r %s/* %s', $src_dir, $dest_dir ); + $shell_command = Utils\esc_cmd( 'cp -r %s/* %s', $src_dir, $dest_dir ); + if ( 'Darwin' === PHP_OS ) { + $shell_command = Utils\esc_cmd( 'cp -R %s/* %s', $src_dir, $dest_dir ); + } elseif ( self::is_windows() ) { + $shell_command = Utils\esc_cmd( 'copy /y %s/* %s', $src_dir, $dest_dir ); + } Process::create( $shell_command )->run_check(); } From a2befafbfa01b85a389a088b8ec3ee129216e908 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:52:14 +0200 Subject: [PATCH 023/107] Use `mysql.exe` --- src/Context/FeatureContext.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index d580b45a9..01ebef865 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -668,6 +668,11 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::$behat_run_dir = getcwd(); self::$mysql_binary = Utils\get_mysql_binary_path(); + // TODO: Improve Windows support upstream in Utils\get_mysql_binary_path(). + if ( self::is_windows() && ! self::$mysql_binary ) { + self::$mysql_binary = 'mysql.exe'; + } + $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); echo "{$result->stdout}\n"; From 497c7a77b436993ada8c787e58fffe8ea28e3cf8 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:55:36 +0200 Subject: [PATCH 024/107] force del --- src/Context/FeatureContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 01ebef865..68b33ab00 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1279,7 +1279,7 @@ public function move_files( $src, $dest ): void { */ public static function remove_dir( $dir ): void { if ( self::is_windows() ) { - Process::create( Utils\esc_cmd( 'del %s', $dir ) )->run_check(); + Process::create( Utils\esc_cmd( 'del /f /q %s', $dir ) )->run_check(); } else { Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); } From 67bd7544cb7f2a7e2682798aa5643ee1902be776 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 09:56:36 +0200 Subject: [PATCH 025/107] fix copy --- src/Context/FeatureContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 68b33ab00..bbf1972b0 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1296,7 +1296,7 @@ public static function copy_dir( $src_dir, $dest_dir ): void { if ( 'Darwin' === PHP_OS ) { $shell_command = Utils\esc_cmd( 'cp -R %s/* %s', $src_dir, $dest_dir ); } elseif ( self::is_windows() ) { - $shell_command = Utils\esc_cmd( 'copy /y %s/* %s', $src_dir, $dest_dir ); + $shell_command = Utils\esc_cmd( 'copy /y %s %s', $src_dir, $dest_dir ); } Process::create( $shell_command )->run_check(); } From d0fb2d230ce89296140d25de7fbc5930bceca6e1 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 10:44:01 +0200 Subject: [PATCH 026/107] Use `runner.debug` --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 838d71d81..150412192 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -327,7 +327,7 @@ jobs: WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' WP_CLI_TEST_COVERAGE: ${{ matrix.coverage }} - BEHAT_ARGS: ${{ matrix.coverage && env.RUNNER_DEBUG && '--debug --format=pretty' || matrix.coverage && '--debug' || env.RUNNER_DEBUG && '--format=pretty' || '' }} + BEHAT_ARGS: ${{ matrix.coverage && runner.debug && '--debug --format=pretty' || matrix.coverage && '--debug' || runner.debug && '--format=pretty' || '' }} run: | composer behat -- $BEHAT_ARGS || composer behat-rerun -- $BEHAT_ARGS From 30d58828893d8b41c826bbcfd87ea345c4043570 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 10:48:09 +0200 Subject: [PATCH 027/107] always pretty for testing --- .github/workflows/testing.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 150412192..1e6f2e75d 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -327,7 +327,8 @@ jobs: WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' WP_CLI_TEST_COVERAGE: ${{ matrix.coverage }} - BEHAT_ARGS: ${{ matrix.coverage && runner.debug && '--debug --format=pretty' || matrix.coverage && '--debug' || runner.debug && '--format=pretty' || '' }} + # BEHAT_ARGS: ${{ matrix.coverage && runner.debug && '--debug --format=pretty' || matrix.coverage && '--debug' || runner.debug && '--format=pretty' || '' }} + BEHAT_ARGS: '--format=pretty' run: | composer behat -- $BEHAT_ARGS || composer behat-rerun -- $BEHAT_ARGS From 0e3551b0879641b4bc5cb8f97fe22f69fb458ea4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 10:48:25 +0200 Subject: [PATCH 028/107] `DIRECTORY_SEPARATOR` all the things --- src/Context/FeatureContext.php | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index bbf1972b0..16c5bcfef 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -568,7 +568,7 @@ private static function get_behat_internal_variables(): array { */ private static function download_sqlite_plugin( $dir ): void { $download_url = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip'; - $download_location = $dir . '/sqlite-database-integration.zip'; + $download_location = $dir . DIRECTORY_SEPARATOR . 'sqlite-database-integration.zip'; if ( ! is_dir( $dir ) ) { mkdir( $dir ); @@ -636,16 +636,16 @@ private static function configure_sqlite( $dir ): void { private static function cache_wp_files(): void { $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; - self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache' . $wp_version_suffix; - self::$sqlite_cache_dir = sys_get_temp_dir() . '/wp-cli-test-sqlite-integration-cache'; + self::$cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-core-download-cache' . $wp_version_suffix; + self::$sqlite_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-sqlite-integration-cache'; if ( 'sqlite' === getenv( 'WP_CLI_TEST_DBTYPE' ) ) { - if ( ! is_readable( self::$sqlite_cache_dir . '/sqlite-database-integration/db.copy' ) ) { + if ( ! is_readable( self::$sqlite_cache_dir . DIRECTORY_SEPARATOR . 'sqlite-database-integration/db.copy' ) ) { self::download_sqlite_plugin( self::$sqlite_cache_dir ); } } - if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) { + if ( is_readable( self::$cache_dir . DIRECTORY_SEPARATOR . 'wp-config-sample.php' ) ) { return; } @@ -679,7 +679,7 @@ public static function prepare( BeforeSuiteScope $scope ): void { // Remove install cache if any (not setting the static var). $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; - $install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; + $install_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-core-install-cache' . $wp_version_suffix; if ( file_exists( $install_cache_dir ) ) { self::remove_dir( $install_cache_dir ); } @@ -806,7 +806,7 @@ public static function create_cache_dir(): string { if ( self::$suite_cache_dir ) { self::remove_dir( self::$suite_cache_dir ); } - self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-suite-cache-' . self::$temp_dir_infix . '-', true ); + self::$suite_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-test-suite-cache-' . self::$temp_dir_infix . '-', true ); mkdir( self::$suite_cache_dir ); return self::$suite_cache_dir; } @@ -1028,7 +1028,7 @@ private static function get_event_file( $scope, &$line ): ?string { */ public function create_run_dir(): void { if ( ! isset( $this->variables['RUN_DIR'] ) ) { - self::$run_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); + self::$run_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); $this->variables['RUN_DIR'] = self::$run_dir; mkdir( $this->variables['RUN_DIR'] ); } @@ -1038,7 +1038,7 @@ public function create_run_dir(): void { * @param string $version */ public function build_phar( $version = 'same' ): void { - $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( 'wp-cli-build-', true ) . '.phar'; + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-build-', true ) . '.phar'; $is_bundle = false; @@ -1061,6 +1061,8 @@ public function build_phar( $version = 'same' ): void { $this->composer_command( 'dump-autoload --working-dir=' . dirname( self::get_vendor_dir() ) ); } + $make_phar_path = realpath( $make_phar_path ); + $this->proc( Utils\esc_cmd( 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', @@ -1109,7 +1111,7 @@ public function download_phar( $version = 'same' ): void { * CACHE_DIR is a cache for downloaded test data such as images. Lives until manually deleted. */ private function set_cache_dir(): void { - $path = sys_get_temp_dir() . '/wp-cli-test-cache'; + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-cache'; if ( ! file_exists( $path ) ) { mkdir( $path ); } @@ -1394,7 +1396,7 @@ public function create_config( $subdir = '', $extra_php = false ): void { public function install_wp( $subdir = '' ): void { $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; - self::$install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; + self::$install_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-core-install-cache' . $wp_version_suffix; if ( ! file_exists( self::$install_cache_dir ) ) { mkdir( self::$install_cache_dir ); } @@ -1507,17 +1509,17 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { public function composer_add_wp_cli_local_repository(): void { if ( ! self::$composer_local_repository ) { - self::$composer_local_repository = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-composer-local-', true ); + self::$composer_local_repository = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-composer-local-', true ); mkdir( self::$composer_local_repository ); $env = self::get_process_env_variables(); $src = $env['TRAVIS_BUILD_DIR'] ?? realpath( self::get_vendor_dir() . '/../' ); - self::copy_dir( $src, self::$composer_local_repository . '/' ); - self::remove_dir( self::$composer_local_repository . '/.git' ); - self::remove_dir( self::$composer_local_repository . '/vendor' ); + self::copy_dir( $src, self::$composer_local_repository . DIRECTORY_SEPARATOR ); + self::remove_dir( self::$composer_local_repository . DIRECTORY_SEPARATOR . '.git' ); + self::remove_dir( self::$composer_local_repository . DIRECTORY_SEPARATOR . 'vendor' ); } - $dest = self::$composer_local_repository . '/'; + $dest = self::$composer_local_repository . DIRECTORY_SEPARATOR; $this->composer_command( "config repositories.wp-cli '{\"type\": \"path\", \"url\": \"$dest\", \"options\": {\"symlink\": false, \"versions\": { \"wp-cli/wp-cli\": \"dev-main\"}}}'" ); $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; } From e04fdfef09dc4db418b7ab2140bad5cebfba630c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 10:50:37 +0200 Subject: [PATCH 029/107] another realpath --- tests/tests/TestBehatTags.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 13ba9c7c0..e67855f22 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -17,7 +17,7 @@ class TestBehatTags extends TestCase { protected function set_up(): void { parent::set_up(); - $this->temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ); + $this->temp_dir = realpath( Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ) ); mkdir( $this->temp_dir ); mkdir( $this->temp_dir . '/features' ); } From 000bd3a622ef711d9d1b2ea90eb311b467356fa3 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 12:54:01 +0200 Subject: [PATCH 030/107] move statement --- tests/tests/TestBehatTags.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index e67855f22..928f2fa63 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -17,9 +17,11 @@ class TestBehatTags extends TestCase { protected function set_up(): void { parent::set_up(); - $this->temp_dir = realpath( Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ) ); + $this->temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ); mkdir( $this->temp_dir ); mkdir( $this->temp_dir . '/features' ); + + $this->temp_dir = realpath( $this->temp_dir ); } protected function tear_down(): void { From 988caa3430c79d834e4247a31ca77445077201e6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 13:00:46 +0200 Subject: [PATCH 031/107] rewrite remove_dir and copy_dir --- src/Context/FeatureContext.php | 41 +++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 16c5bcfef..02291deba 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1280,11 +1280,24 @@ public function move_files( $src, $dest ): void { * @param string $dir */ public static function remove_dir( $dir ): void { - if ( self::is_windows() ) { - Process::create( Utils\esc_cmd( 'del /f /q %s', $dir ) )->run_check(); - } else { - Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + if ( ! file_exists( $dir ) ) { + return; } + + if ( ! is_dir( $dir ) ) { + unlink( $dir ); + return; + } + + foreach ( scandir( $dir ) as $item ) { + if ( '.' === $item || '..' === $item ) { + continue; + } + + self::remove_dir( $dir . DIRECTORY_SEPARATOR . $item ); + } + + rmdir( $dir ); } /** @@ -1294,13 +1307,21 @@ public static function remove_dir( $dir ): void { * @param string $dest_dir */ public static function copy_dir( $src_dir, $dest_dir ): void { - $shell_command = Utils\esc_cmd( 'cp -r %s/* %s', $src_dir, $dest_dir ); - if ( 'Darwin' === PHP_OS ) { - $shell_command = Utils\esc_cmd( 'cp -R %s/* %s', $src_dir, $dest_dir ); - } elseif ( self::is_windows() ) { - $shell_command = Utils\esc_cmd( 'copy /y %s %s', $src_dir, $dest_dir ); + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $src_dir, \RecursiveDirectoryIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ( $iterator as $item ) { + $dest_path = $dest_dir . DIRECTORY_SEPARATOR . $iterator->getSubPathname(); + if ( $item->isDir() ) { + if ( ! is_dir( $dest_path ) ) { + mkdir( $dest_path, 0777, true ); + } + } else { + copy( $item->getPathname(), $dest_path ); + } } - Process::create( $shell_command )->run_check(); } /** From f7b690f55d1becbd917ee05f544fb559b29155eb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 13:08:34 +0200 Subject: [PATCH 032/107] Replace curl cli with `http_request` --- src/Context/FeatureContext.php | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 02291deba..e7139cc78 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -574,15 +574,11 @@ private static function download_sqlite_plugin( $dir ): void { mkdir( $dir ); } - $curl = self::is_windows() ? 'curl.exe' : 'curl'; + $response = Utils\http_request( 'GET', $download_url, null, [], [ 'filename' => $download_location ] ); - Process::create( - Utils\esc_cmd( - "$curl -sSfL %1\$s > %2\$s", - $download_url, - $download_location - ) - )->run_check(); + if ( 200 !== $response->status_code ) { + throw new RuntimeException( "Could not download SQLite plugin (HTTP code {$response->status_code})" ); + } $zip = new \ZipArchive(); $new_zip_file = $download_location; @@ -1096,15 +1092,11 @@ public function download_phar( $version = 'same' ): void { . uniqid( 'wp-cli-download-', true ) . '.phar'; - $curl = self::is_windows() ? 'curl.exe' : 'curl'; + $response = Utils\http_request( 'GET', $download_url, null, [], [ 'filename' => $this->variables['PHAR_PATH'] ] ); - Process::create( - Utils\esc_cmd( - "$curl -sSfL %1\$s > %2\$s && chmod +x %2\$s", - $download_url, - $this->variables['PHAR_PATH'] - ) - )->run_check(); + if ( 200 !== $response->status_code ) { + throw new RuntimeException( "Could not download WP-CLI PHAR (HTTP code {$response->status_code})" ); + } } /** From 6bf0301ea64eb393359fa3e579ab41471a3979cb Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 13:34:46 +0200 Subject: [PATCH 033/107] update phpunit config --- phpunit.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b7619f503..97c0c774b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,7 +11,6 @@ > - tests/ tests/tests From 3456a02e7840a9e1f888664f8681cad51c613d57 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 13:34:53 +0200 Subject: [PATCH 034/107] remove method --- src/Context/FeatureContext.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index e7139cc78..a7ec7b3a9 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -416,15 +416,6 @@ public static function get_bin_path(): ?string { return $bin_path; } - /** - * Whether the current OS is Windows. - * - * @return bool - */ - private static function is_windows(): bool { - return strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; - } - /** * Get the environment variables required for launched `wp` processes. * @@ -665,7 +656,7 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::$mysql_binary = Utils\get_mysql_binary_path(); // TODO: Improve Windows support upstream in Utils\get_mysql_binary_path(). - if ( self::is_windows() && ! self::$mysql_binary ) { + if ( Utils\is_windows() && ! self::$mysql_binary ) { self::$mysql_binary = 'mysql.exe'; } From 676c91884cb7e2070fa30b477badc9b85444ed35 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 14:23:38 +0200 Subject: [PATCH 035/107] undo some changes --- src/Context/FeatureContext.php | 36 +++++++++++++++++----------------- tests/tests/TestBehatTags.php | 6 ++---- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index a7ec7b3a9..871a5cd18 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -559,7 +559,7 @@ private static function get_behat_internal_variables(): array { */ private static function download_sqlite_plugin( $dir ): void { $download_url = 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip'; - $download_location = $dir . DIRECTORY_SEPARATOR . 'sqlite-database-integration.zip'; + $download_location = $dir . '/sqlite-database-integration.zip'; if ( ! is_dir( $dir ) ) { mkdir( $dir ); @@ -623,16 +623,16 @@ private static function configure_sqlite( $dir ): void { private static function cache_wp_files(): void { $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; - self::$cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-core-download-cache' . $wp_version_suffix; - self::$sqlite_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-sqlite-integration-cache'; + self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache' . $wp_version_suffix; + self::$sqlite_cache_dir = sys_get_temp_dir() . '/wp-cli-test-sqlite-integration-cache'; if ( 'sqlite' === getenv( 'WP_CLI_TEST_DBTYPE' ) ) { - if ( ! is_readable( self::$sqlite_cache_dir . DIRECTORY_SEPARATOR . 'sqlite-database-integration/db.copy' ) ) { + if ( ! is_readable( self::$sqlite_cache_dir . '/sqlite-database-integration/db.copy' ) ) { self::download_sqlite_plugin( self::$sqlite_cache_dir ); } } - if ( is_readable( self::$cache_dir . DIRECTORY_SEPARATOR . 'wp-config-sample.php' ) ) { + if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) { return; } @@ -666,7 +666,7 @@ public static function prepare( BeforeSuiteScope $scope ): void { // Remove install cache if any (not setting the static var). $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; - $install_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-core-install-cache' . $wp_version_suffix; + $install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; if ( file_exists( $install_cache_dir ) ) { self::remove_dir( $install_cache_dir ); } @@ -793,7 +793,7 @@ public static function create_cache_dir(): string { if ( self::$suite_cache_dir ) { self::remove_dir( self::$suite_cache_dir ); } - self::$suite_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-test-suite-cache-' . self::$temp_dir_infix . '-', true ); + self::$suite_cache_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-suite-cache-' . self::$temp_dir_infix . '-', true ); mkdir( self::$suite_cache_dir ); return self::$suite_cache_dir; } @@ -1015,7 +1015,7 @@ private static function get_event_file( $scope, &$line ): ?string { */ public function create_run_dir(): void { if ( ! isset( $this->variables['RUN_DIR'] ) ) { - self::$run_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); + self::$run_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); $this->variables['RUN_DIR'] = self::$run_dir; mkdir( $this->variables['RUN_DIR'] ); } @@ -1025,7 +1025,7 @@ public function create_run_dir(): void { * @param string $version */ public function build_phar( $version = 'same' ): void { - $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-build-', true ) . '.phar'; + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( 'wp-cli-build-', true ) . '.phar'; $is_bundle = false; @@ -1094,7 +1094,7 @@ public function download_phar( $version = 'same' ): void { * CACHE_DIR is a cache for downloaded test data such as images. Lives until manually deleted. */ private function set_cache_dir(): void { - $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-cache'; + $path = sys_get_temp_dir() . '/wp-cli-test-cache'; if ( ! file_exists( $path ) ) { mkdir( $path ); } @@ -1277,7 +1277,7 @@ public static function remove_dir( $dir ): void { continue; } - self::remove_dir( $dir . DIRECTORY_SEPARATOR . $item ); + self::remove_dir( $dir . '/' . $item ); } rmdir( $dir ); @@ -1296,7 +1296,7 @@ public static function copy_dir( $src_dir, $dest_dir ): void { ); foreach ( $iterator as $item ) { - $dest_path = $dest_dir . DIRECTORY_SEPARATOR . $iterator->getSubPathname(); + $dest_path = $dest_dir . '/' . $iterator->getSubPathname(); if ( $item->isDir() ) { if ( ! is_dir( $dest_path ) ) { mkdir( $dest_path, 0777, true ); @@ -1400,7 +1400,7 @@ public function create_config( $subdir = '', $extra_php = false ): void { public function install_wp( $subdir = '' ): void { $wp_version = getenv( 'WP_VERSION' ); $wp_version_suffix = ( false !== $wp_version ) ? "-$wp_version" : ''; - self::$install_cache_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'wp-cli-test-core-install-cache' . $wp_version_suffix; + self::$install_cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-install-cache' . $wp_version_suffix; if ( ! file_exists( self::$install_cache_dir ) ) { mkdir( self::$install_cache_dir ); } @@ -1513,17 +1513,17 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { public function composer_add_wp_cli_local_repository(): void { if ( ! self::$composer_local_repository ) { - self::$composer_local_repository = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-composer-local-', true ); + self::$composer_local_repository = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-composer-local-', true ); mkdir( self::$composer_local_repository ); $env = self::get_process_env_variables(); $src = $env['TRAVIS_BUILD_DIR'] ?? realpath( self::get_vendor_dir() . '/../' ); - self::copy_dir( $src, self::$composer_local_repository . DIRECTORY_SEPARATOR ); - self::remove_dir( self::$composer_local_repository . DIRECTORY_SEPARATOR . '.git' ); - self::remove_dir( self::$composer_local_repository . DIRECTORY_SEPARATOR . 'vendor' ); + self::copy_dir( $src, self::$composer_local_repository . '/' ); + self::remove_dir( self::$composer_local_repository . '/.git' ); + self::remove_dir( self::$composer_local_repository . '/vendor' ); } - $dest = self::$composer_local_repository . DIRECTORY_SEPARATOR; + $dest = self::$composer_local_repository . '/'; $this->composer_command( "config repositories.wp-cli '{\"type\": \"path\", \"url\": \"$dest\", \"options\": {\"symlink\": false, \"versions\": { \"wp-cli/wp-cli\": \"dev-main\"}}}'" ); $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; } diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 928f2fa63..d5b4537aa 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -20,8 +20,6 @@ protected function set_up(): void { $this->temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ); mkdir( $this->temp_dir ); mkdir( $this->temp_dir . '/features' ); - - $this->temp_dir = realpath( $this->temp_dir ); } protected function tear_down(): void { @@ -179,7 +177,7 @@ public function test_behat_tags_extension(): void { putenv( 'GITHUB_TOKEN' ); - $behat_tags = realpath( dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php' ); + $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; file_put_contents( $this->temp_dir . '/features/extension.feature', '@require-extension-imagick @require-extension-curl' ); @@ -219,7 +217,7 @@ public function test_behat_tags_extension(): void { public function test_behat_tags_db_version(): void { $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); - $behat_tags = realpath( dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php' ); + $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; // Just to get the get_db_version() function. ob_start(); From d88296bac086634fff243dacf7299c2ce56f67e5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 3 Oct 2025 14:54:56 +0200 Subject: [PATCH 036/107] Cleanup --- .github/workflows/testing.yml | 338 +-------------------------------- src/Context/FeatureContext.php | 7 - tests/tests/TestBehatTags.php | 2 +- 3 files changed, 3 insertions(+), 344 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 1e6f2e75d..bf67592d8 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -10,340 +10,6 @@ on: schedule: - cron: '17 1 * * *' # Run every day on a seemly random time. -# Cancels all previous workflow runs for the same branch that have not yet completed. -concurrency: - # The concurrency group contains the workflow name and the branch name. - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: - get-matrix: - name: Get base test matrix - runs-on: ubuntu-22.04 - outputs: - matrix: ${{ steps.base-matrix.outputs.matrix }} - steps: - - name: Set matrix - id: base-matrix - run: | - MATRIX=$(cat << EOF - { - "include": [ - { - "php": "8.4", - "wp": "latest", - "mysql": "mysql-8.0" - }, - { - "php": "8.4", - "wp": "latest", - "dbtype": "sqlite" - }, - { - "php": "8.4", - "wp": "latest", - "mysql": "mysql-8.0", - "os": "macos-15" - }, - { - "php": "8.4", - "wp": "latest", - "dbtype": "sqlite", - "os": "macos-15" - }, - { - "php": "8.4", - "wp": "latest", - "mysql": "mysql-8.0", - "os": "windows-2025" - }, - { - "php": "8.4", - "wp": "latest", - "dbtype": "sqlite", - "os": "windows-2025" - } - ] - } - EOF - ) - echo matrix=$MATRIX >> $GITHUB_OUTPUT - - prepare-unit: - name: Prepare matrix for unit tests - needs: get-matrix - runs-on: ubuntu-22.04 - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Check out source code - uses: actions/checkout@v5 - - - name: Check existence of composer.json & phpunit.xml.dist files - id: check_files - uses: andstor/file-existence-action@v3 - with: - files: "composer.json, phpunit.xml.dist" - - - name: Set matrix - id: set-matrix - run: | - if [[ $FILE_EXISTS == 'true' ]]; then - echo "matrix=$(jq -c \ - --argjson with_coverage_flag "true" \ - --arg minimum_php "7.2" \ - --arg minimum_wp "4.9" \ - ' - .include |= ( - map( - # First, select only the versions that meet all minimum requirements - select( - (.php >= $minimum_php) and - (.wp == "latest" or .wp >= $minimum_wp) - ) | - - # Next, update the coverage flag on the remaining items - if $with_coverage_flag == false and .coverage == true then - .coverage = false - else - . - end - ) | - - # Finally, get the unique entries - unique_by([.php, .os]) - ) - ' <<< "$BASE_MATRIX")" >> $GITHUB_OUTPUT - else - echo "matrix=" >> $GITHUB_OUTPUT - fi - env: - BASE_MATRIX: ${{ needs.get-matrix.outputs.matrix }} - FILE_EXISTS: ${{ steps.check_files.outputs.files_exists == 'true' }} - - unit: #----------------------------------------------------------------------- - needs: prepare-unit - if: ${{ needs.prepare-unit.outputs.matrix != '' }} - name: Unit test / PHP ${{ matrix.php }}${{ matrix.coverage && ' (with coverage)' || '' }} (${{ matrix.os || 'ubuntu-22.04' }}) - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.prepare-unit.outputs.matrix) }} - runs-on: ${{ matrix.os || 'ubuntu-22.04' }} - - continue-on-error: ${{ matrix.php == 'nightly' }} - - steps: - - name: Check out source code - uses: actions/checkout@v5 - - - name: Set up PHP environment - uses: shivammathur/setup-php@v2 - with: - php-version: '${{ matrix.php }}' - ini-values: zend.assertions=1, error_reporting=-1, display_errors=On - coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} - tools: composer,cs2pr - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Composer dependencies & cache dependencies - uses: "ramsey/composer-install@v3" - env: - COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} - with: - # Bust the cache at least once a month - output format: YYYY-MM. - custom-cache-suffix: $(date -u "+%Y-%m") - - # PHPUnit 10+ may fail a test run when the "old" configuration format is used. - # Luckily, there is a build-in migration tool since PHPUnit 9.3. - - name: Migrate PHPUnit configuration for PHPUnit 10+ - if: ${{ matrix.php >= 8.2 || matrix.php == 'nightly' }} - continue-on-error: true - run: composer phpunit -- --migrate-configuration - - - name: Setup problem matcher to provide annotations for PHPUnit - run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - - - name: Run PHPUnit with coverage - if: ${{ matrix.coverage }} - run: | - composer phpunit -- --coverage-clover build/logs/unit-coverage.xml - - - name: Run PHPUnit - if: ${{ ! matrix.coverage }} - # For example TestBehatTags.php in wp-cli-tests depends on the db type. - env: - WP_CLI_TEST_DBTYPE: 'sqlite' - run: | - composer phpunit - - - name: Upload code coverage report - if: ${{ matrix.coverage }} - uses: codecov/codecov-action@v5.5.1 - with: - directory: build/logs - flags: unit - token: ${{ secrets.CODECOV_TOKEN }} - - prepare-functional: #--------------------------------------------------------- - name: Prepare matrix for functional tests - needs: get-matrix - runs-on: ubuntu-22.04 - outputs: - matrix: ${{ steps.set-matrix.outputs.matrix }} - steps: - - name: Check out source code - uses: actions/checkout@v5 - - - name: Check existence of composer.json & behat.yml files - id: check_files - uses: andstor/file-existence-action@v3 - with: - files: "composer.json, behat.yml" - - - name: Set matrix - id: set-matrix - run: | - if [[ $FILE_EXISTS == 'true' ]]; then - echo "matrix=$(jq -c \ - --argjson with_coverage_flag "true" \ - --arg minimum_php "7.2" \ - --arg minimum_wp "4.9" \ - ' - # First, select only the versions that meet all minimum requirements - .include |= ( - map( - select( - .php >= $minimum_php - ) | - # Next, update the coverage flag on the remaining items - if $with_coverage_flag == false and .coverage == true then - .coverage = false - else - . - end - ) - ) | - - # Reassign WP4.9 to minimum_wp - .include |= ( - map( - select( - .wp == "4.9" - ).wp |= $minimum_wp - ) - ) - ' <<< "$BASE_MATRIX" )" >> $GITHUB_OUTPUT - else - echo "matrix=" >> $GITHUB_OUTPUT - fi - env: - BASE_MATRIX: ${{ needs.get-matrix.outputs.matrix }} - FILE_EXISTS: ${{ steps.check_files.outputs.files_exists == 'true' }} - - functional: #----------------------------------------------------------------- - needs: prepare-functional - if: ${{ needs.prepare-functional.outputs.matrix != '' }} - name: Functional - WP ${{ matrix.wp }} on PHP ${{ matrix.php }} with ${{ matrix.dbtype != 'sqlite' && matrix.mysql || 'SQLite' }}${{ matrix.coverage && ' (with coverage)' || '' }} (${{ matrix.os || 'ubuntu-22.04' }}) - strategy: - fail-fast: false - matrix: ${{ fromJson(needs.prepare-functional.outputs.matrix) }} - runs-on: ${{ matrix.os || 'ubuntu-22.04' }} - - continue-on-error: ${{ matrix.dbtype == 'sqlite' || matrix.dbtype == 'mariadb' || matrix.php == 'nightly' }} - - env: - MYSQL_HOST: 127.0.0.1 - MYSQL_TCP_PORT: 3306 - WP_CLI_TEST_DBROOTUSER: root - WP_CLI_TEST_DBROOTPASS: root - WP_CLI_TEST_DBNAME: wp_cli_test - WP_CLI_TEST_DBUSER: wp_cli_test - WP_CLI_TEST_DBPASS: password1 - WP_CLI_TEST_DBHOST: 127.0.0.1:3306 - - steps: - - name: Check out source code - uses: actions/checkout@v5 - - - name: Install Ghostscript - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == '' }} - run: | - sudo apt-get update - sudo apt-get install ghostscript -y - - - name: Set up PHP environment - uses: shivammathur/setup-php@v2 - with: - php-version: '${{ matrix.php }}' - ini-values: zend.assertions=1, error_reporting=-1, display_errors=On - extensions: gd, imagick, mysql, zip - coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} - tools: composer - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Change ImageMagick policy to allow pdf->png conversion. - if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == '' }} - run: | - sudo sed -i 's/^.*policy.*coder.*none.*PDF.*//' /etc/ImageMagick-6/policy.xml - - - name: Install Composer dependencies & cache dependencies - uses: "ramsey/composer-install@v3" - env: - COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} - with: - # Bust the cache at least once a month - output format: YYYY-MM. - custom-cache-suffix: $(date -u "+%Y-%m") - - - name: Setup MySQL Server - id: setup-mysql - if: ${{ matrix.dbtype != 'sqlite' }} - uses: shogo82148/actions-setup-mysql@v1 - with: - mysql-version: ${{ matrix.mysql }} - auto-start: true - root-password: ${{ env.WP_CLI_TEST_DBROOTPASS }} - user: ${{ env.WP_CLI_TEST_DBUSER}} - password: ${{ env.WP_CLI_TEST_DBPASS}} - my-cnf: | - default_authentication_plugin=mysql_native_password - - - name: Prepare test database - if: ${{ matrix.dbtype != 'sqlite' }} - run: composer prepare-tests - - - name: Check Behat environment - env: - WP_VERSION: '${{ matrix.wp }}' - WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} - WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' - WP_CLI_TEST_DEBUG_BEHAT_ENV: 1 - run: composer behat - - - name: Run Behat - env: - WP_VERSION: '${{ matrix.wp }}' - WP_CLI_TEST_DBTYPE: ${{ matrix.dbtype || 'mysql' }} - WP_CLI_TEST_DBSOCKET: '${{ steps.setup-mysql.outputs.base-dir }}/tmp/mysql.sock' - WP_CLI_TEST_COVERAGE: ${{ matrix.coverage }} - # BEHAT_ARGS: ${{ matrix.coverage && runner.debug && '--debug --format=pretty' || matrix.coverage && '--debug' || runner.debug && '--format=pretty' || '' }} - BEHAT_ARGS: '--format=pretty' - run: | - composer behat -- $BEHAT_ARGS || composer behat-rerun -- $BEHAT_ARGS - - - name: Retrieve list of coverage files - id: coverage_files - if: ${{ matrix.coverage }} - run: | - FILES=$(find "$GITHUB_WORKSPACE/build/logs" -path '*.*' | paste -s -d "," -) - echo "files=$FILES" >> $GITHUB_OUTPUT - - - name: Upload code coverage report - if: ${{ matrix.coverage }} - uses: codecov/codecov-action@v5.5.1 - with: - # Because somehow providing `directory: build/logs` doesn't work for these files - files: ${{ steps.coverage_files.outputs.files }} - flags: feature - token: ${{ secrets.CODECOV_TOKEN }} + test: + uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 871a5cd18..4ae8c4dc7 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -655,11 +655,6 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::$behat_run_dir = getcwd(); self::$mysql_binary = Utils\get_mysql_binary_path(); - // TODO: Improve Windows support upstream in Utils\get_mysql_binary_path(). - if ( Utils\is_windows() && ! self::$mysql_binary ) { - self::$mysql_binary = 'mysql.exe'; - } - $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); echo "{$result->stdout}\n"; @@ -1048,8 +1043,6 @@ public function build_phar( $version = 'same' ): void { $this->composer_command( 'dump-autoload --working-dir=' . dirname( self::get_vendor_dir() ) ); } - $make_phar_path = realpath( $make_phar_path ); - $this->proc( Utils\esc_cmd( 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index d5b4537aa..0e8be1040 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -219,7 +219,7 @@ public function test_behat_tags_db_version(): void { $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - // Just to get the get_db_version() function. + // Just to get the get_db_version() function. Prevents unexpected output. ob_start(); require $behat_tags; ob_end_clean(); From 8f41d68f5d45c05a255776068d1846fa0672c759 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 4 Oct 2025 16:56:21 +0200 Subject: [PATCH 037/107] Add custom matrix --- .github/workflows/testing.yml | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index bf67592d8..dec9fe2f8 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -12,4 +12,35 @@ on: jobs: test: - uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main + uses: wp-cli/.github/.github/workflows/reusable-testing.yml@try/test-suite + with: + matrix: | + { + "include": [ + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0", + "os": "macos-15" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite", + "os": "macos-15" + }, + { + "php": "8.4", + "wp": "latest", + "mysql": "mysql-8.0", + "os": "windows-2025" + }, + { + "php": "8.4", + "wp": "latest", + "dbtype": "sqlite", + "os": "windows-2025" + } + ], + "exclude": [] + } From f7c67eb87b2f0ed1c1a645c26b2294ae6b7f404e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 4 Oct 2025 17:02:53 +0200 Subject: [PATCH 038/107] Add back todo --- src/Context/FeatureContext.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 4ae8c4dc7..3e6884a8f 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -655,6 +655,11 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::$behat_run_dir = getcwd(); self::$mysql_binary = Utils\get_mysql_binary_path(); + // TODO: Improve Windows support upstream in Utils\get_mysql_binary_path(). + if ( Utils\is_windows() && ! self::$mysql_binary ) { + self::$mysql_binary = 'mysql.exe'; + } + $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); echo "{$result->stdout}\n"; From a071f36d40dbef10d09464bb56314a03d33f2ee2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sat, 4 Oct 2025 22:48:52 +0200 Subject: [PATCH 039/107] use iterator --- src/Context/FeatureContext.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 3e6884a8f..fb21512ca 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1261,21 +1261,21 @@ public function move_files( $src, $dest ): void { * @param string $dir */ public static function remove_dir( $dir ): void { - if ( ! file_exists( $dir ) ) { - return; - } - if ( ! is_dir( $dir ) ) { - unlink( $dir ); return; } - foreach ( scandir( $dir ) as $item ) { - if ( '.' === $item || '..' === $item ) { - continue; - } + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); - self::remove_dir( $dir . '/' . $item ); + foreach ( $iterator as $file ) { + if ( $file->isDir() ) { + rmdir( $file->getRealPath() ); + } else { + unlink( $file->getRealPath() ); + } } rmdir( $dir ); From d1aada28c1f07b456cef925db538ef42b3664c60 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 10 Oct 2025 23:08:28 +0200 Subject: [PATCH 040/107] Use `taskkill` on Windows --- src/Context/FeatureContext.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index fb21512ca..683597ff2 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -763,6 +763,10 @@ public function afterScenario( AfterScenarioScope $scope ): void { * @param int $master_pid */ private static function terminate_proc( $master_pid ): void { + if ( Utils\is_windows() ) { + proc_close( proc_open( "taskkill /F /T /PID $master_pid", [], $pipes ) ); + return; + } $output = shell_exec( "ps -o ppid,pid,command | grep $master_pid" ); From 94ba759dbaa524b361e612fe04cbc23cf0c6a24d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 10 Oct 2025 23:23:25 +0200 Subject: [PATCH 041/107] second attempt Props Gemini CLI --- src/Context/FeatureContext.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 683597ff2..6bd77002c 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -764,7 +764,7 @@ public function afterScenario( AfterScenarioScope $scope ): void { */ private static function terminate_proc( $master_pid ): void { if ( Utils\is_windows() ) { - proc_close( proc_open( "taskkill /F /T /PID $master_pid", [], $pipes ) ); + shell_exec( "taskkill /F /T /PID $master_pid > NUL 2>&1" ); return; } @@ -781,6 +781,10 @@ private static function terminate_proc( $master_pid ): void { } } + if ( ! function_exists( 'posix_kill' ) ) { + return; + } + if ( ! posix_kill( (int) $master_pid, 9 ) ) { $errno = posix_get_last_error(); // Ignore "No such process" error as that's what we want. From a3804a6e3f8fdbb4ed81c33709657f1c18fdba6a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 10 Oct 2025 23:31:14 +0200 Subject: [PATCH 042/107] no grep --- src/Context/FeatureContext.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 6bd77002c..062d45e88 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1455,7 +1455,8 @@ public function install_wp( $subdir = '' ): void { if ( 'sqlite' !== self::$db_type ) { $mysqldump_binary = Utils\get_sql_dump_command(); $mysqldump_binary = Utils\force_env_on_nix_systems( $mysqldump_binary ); - $support_column_statistics = exec( "{$mysqldump_binary} --help | grep 'column-statistics'" ); + $help_output = shell_exec( "{$mysqldump_binary} --help" ); + $support_column_statistics = false !== strpos( $help_output, 'column-statistics' ); $command = "{$mysqldump_binary} --no-defaults --no-tablespaces"; if ( $support_column_statistics ) { $command .= ' --skip-column-statistics'; From 4b71eb388cb1381641d1318a71a989af18c01e36 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Fri, 10 Oct 2025 23:37:55 +0200 Subject: [PATCH 043/107] more ai fixes --- src/Context/FeatureContext.php | 34 +++++++++++++++++++--------- src/Context/GivenStepDefinitions.php | 5 +++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 062d45e88..8bf447eb1 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -653,11 +653,12 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::log_run_times_before_suite( $scope ); } self::$behat_run_dir = getcwd(); - self::$mysql_binary = Utils\get_mysql_binary_path(); // TODO: Improve Windows support upstream in Utils\get_mysql_binary_path(). - if ( Utils\is_windows() && ! self::$mysql_binary ) { + if ( Utils\is_windows() ) { self::$mysql_binary = 'mysql.exe'; + } else { + self::$mysql_binary = Utils\get_mysql_binary_path(); } $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); @@ -1056,14 +1057,18 @@ public function build_phar( $version = 'same' ): void { $this->composer_command( 'dump-autoload --working-dir=' . dirname( self::get_vendor_dir() ) ); } - $this->proc( - Utils\esc_cmd( - 'php -dphar.readonly=0 %1$s %2$s --version=%3$s && chmod +x %2$s', - $make_phar_path, - $this->variables['PHAR_PATH'], - $version - ) - )->run_check(); + $command = Utils\esc_cmd( + 'php -dphar.readonly=0 %1$s %2$s --version=%3$s', + $make_phar_path, + $this->variables['PHAR_PATH'], + $version + ); + + if ( ! Utils\is_windows() ) { + $command .= Utils\esc_cmd( ' && chmod +x %s', $this->variables['PHAR_PATH'] ); + } + + $this->proc( $command )->run_check(); // Revert the suffix change again if ( $is_bundle && self::running_with_code_coverage() ) { @@ -1565,7 +1570,14 @@ public function start_php_server( $subdir = '' ): void { */ private function composer_command( $cmd ): void { if ( ! isset( $this->variables['COMPOSER_PATH'] ) ) { - $this->variables['COMPOSER_PATH'] = exec( 'which composer' ); + $command = Utils\is_windows() ? 'where composer' : 'which composer'; + $path = exec( $command ); + if ( false === $path ) { + throw new RuntimeException( 'Could not find composer.' ); + } + // In case of multiple paths, pick the first one. + $path = strtok( $path, PHP_EOL ); + $this->variables['COMPOSER_PATH'] = $path; } $this->proc( $this->variables['COMPOSER_PATH'] . ' --no-interaction ' . $cmd )->run_check(); } diff --git a/src/Context/GivenStepDefinitions.php b/src/Context/GivenStepDefinitions.php index 248766a0e..5deca4b73 100644 --- a/src/Context/GivenStepDefinitions.php +++ b/src/Context/GivenStepDefinitions.php @@ -604,7 +604,10 @@ public function given_a_download( TableNode $table ): void { continue; } - Process::create( Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check(); + $response = Utils\http_request( 'GET', $row['url'], null, [], [ 'filename' => $path ] ); + if ( 200 !== $response->status_code ) { + throw new RuntimeException( "Could not download file (HTTP code {$response->status_code})" ); + } } } From 56c1086aa5284b06c264d4a63e0c893a00fc2ddc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 12:31:51 +0200 Subject: [PATCH 044/107] undo color check --- bin/install-package-tests | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/bin/install-package-tests b/bin/install-package-tests index 6821677de..ec3916cdd 100755 --- a/bin/install-package-tests +++ b/bin/install-package-tests @@ -16,17 +16,10 @@ is_numeric() { esac } # Prompt color vars. -C_RED="\033[31m" -C_BLUE="\033[34m" +C_RED="\033[0;31m" +C_BLUE="\033[0;34m" NO_FORMAT="\033[0m" -# If running in CI, don't use colors. -if [ -n "${CI}" ]; then - C_RED="" - C_BLUE="" - NO_FORMAT="" -fi - HOST=localhost PORT="" HOST_STRING='' From 4859f9d087e923aa17f17b669d3dcd2205bfbdf5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 12:56:17 +0200 Subject: [PATCH 045/107] Create `wp.bat` on Windows --- src/Context/FeatureContext.php | 41 +++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 8bf447eb1..5b267c264 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -406,10 +406,28 @@ public static function get_bin_path(): ?string { self::get_framework_dir() . '/bin', ]; - foreach ( $bin_paths as $path ) { - if ( is_file( "{$path}/wp" ) && is_executable( "{$path}/wp" ) ) { - $bin_path = $path; - break; + if ( Utils\is_windows() ) { + foreach ( $bin_paths as $path ) { + $wp_script_path = $path . DIRECTORY_SEPARATOR . 'wp'; + $wp_bat_path = $path . DIRECTORY_SEPARATOR . 'wp.bat'; + + if ( is_file( $wp_script_path ) ) { + if ( ! is_file( $wp_bat_path ) ) { + $bat_content = '@ECHO OFF' . PHP_EOL; + $bat_content .= 'php "' . realpath( $wp_script_path ) . '" %*'; + file_put_contents( $wp_bat_path, $bat_content ); + } + $bin_path = $path; + break; + } + } + } else { + foreach ( $bin_paths as $path ) { + $full_bin_path = $path . DIRECTORY_SEPARATOR . 'wp'; + if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { + $bin_path = $path; + break; + } } } @@ -430,14 +448,21 @@ private static function get_process_env_variables(): array { // Ensure we're using the expected `wp` binary. $bin_path = self::get_bin_path(); + + if ( ! $bin_path ) { + throw new RuntimeException( 'Could not find WP-CLI binary path.' ); + } + wp_cli_behat_env_debug( "WP-CLI binary path: {$bin_path}" ); - if ( ! file_exists( "{$bin_path}/wp" ) ) { - wp_cli_behat_env_debug( "WARNING: No file named 'wp' found in the provided/detected binary path." ); + $executable = Utils\is_windows() ? $bin_path . DIRECTORY_SEPARATOR . 'wp.bat' : $bin_path . DIRECTORY_SEPARATOR . 'wp'; + + if ( ! file_exists( $executable ) ) { + wp_cli_behat_env_debug( "WARNING: File $executable not found." ); } - if ( ! is_executable( "{$bin_path}/wp" ) ) { - wp_cli_behat_env_debug( "WARNING: File named 'wp' found in the provided/detected binary path is not executable." ); + if ( ! is_executable( $executable ) ) { + wp_cli_behat_env_debug( "WARNING: File $executable is not executable." ); } $path_separator = Utils\is_windows() ? ';' : ':'; From 4dc4baca42865e4ab60e49dd614abe8a82ccb8e2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:07:50 +0200 Subject: [PATCH 046/107] Partial revert --- src/Context/FeatureContext.php | 58 +++++++++++++--------------------- 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 5b267c264..ae58a2789 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -324,9 +324,9 @@ public static function get_vendor_dir(): ?string { // We try to detect the vendor folder in the most probable locations. $vendor_locations = [ // wp-cli/wp-cli-tests is a dependency of the current working dir. - getcwd() . '/vendor', + getcwd() . DIRECTORY_SEPARATOR . 'vendor', // wp-cli/wp-cli-tests is the root project. - dirname( __DIR__, 2 ) . '/vendor', + dirname( __DIR__, 2 ) . DIRECTORY_SEPARATOR . 'vendor', // wp-cli/wp-cli-tests is a dependency. dirname( __DIR__, 4 ), ]; @@ -365,7 +365,7 @@ public static function get_framework_dir(): ?string { // wp-cli/wp-cli is the root project. dirname( $vendor_folder ), // wp-cli/wp-cli is a dependency. - "{$vendor_folder}/wp-cli/wp-cli", + $vendor_folder . DIRECTORY_SEPARATOR . 'wp-cli' . DIRECTORY_SEPARATOR . 'wp-cli', ]; $framework_folder = ''; @@ -402,32 +402,17 @@ public static function get_bin_path(): ?string { } $bin_paths = [ - self::get_vendor_dir() . '/bin', - self::get_framework_dir() . '/bin', + self::get_vendor_dir() . DIRECTORY_SEPARATOR . 'bin', + self::get_framework_dir() . DIRECTORY_SEPARATOR . 'bin', ]; - if ( Utils\is_windows() ) { - foreach ( $bin_paths as $path ) { - $wp_script_path = $path . DIRECTORY_SEPARATOR . 'wp'; - $wp_bat_path = $path . DIRECTORY_SEPARATOR . 'wp.bat'; - - if ( is_file( $wp_script_path ) ) { - if ( ! is_file( $wp_bat_path ) ) { - $bat_content = '@ECHO OFF' . PHP_EOL; - $bat_content .= 'php "' . realpath( $wp_script_path ) . '" %*'; - file_put_contents( $wp_bat_path, $bat_content ); - } - $bin_path = $path; - break; - } - } - } else { - foreach ( $bin_paths as $path ) { - $full_bin_path = $path . DIRECTORY_SEPARATOR . 'wp'; - if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { - $bin_path = $path; - break; - } + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; + + foreach ( $bin_paths as $path ) { + $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; + if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { + $bin_path = $path; + break; } } @@ -455,14 +440,14 @@ private static function get_process_env_variables(): array { wp_cli_behat_env_debug( "WP-CLI binary path: {$bin_path}" ); - $executable = Utils\is_windows() ? $bin_path . DIRECTORY_SEPARATOR . 'wp.bat' : $bin_path . DIRECTORY_SEPARATOR . 'wp'; + $bin = $bin_path . DIRECTORY_SEPARATOR . ( Utils\is_windows() ? 'wp.bat' : 'wp' ); - if ( ! file_exists( $executable ) ) { - wp_cli_behat_env_debug( "WARNING: File $executable not found." ); + if ( ! file_exists( $bin ) ) { + wp_cli_behat_env_debug( "WARNING: File $bin not found." ); } - if ( ! is_executable( $executable ) ) { - wp_cli_behat_env_debug( "WARNING: File $executable is not executable." ); + if ( ! is_executable( $bin ) ) { + wp_cli_behat_env_debug( "WARNING: File $bin is not executable." ); } $path_separator = Utils\is_windows() ? ';' : ':'; @@ -933,18 +918,19 @@ private function replace_invoke_wp_cli_with_php_args( $str ) { $phar_begin = '#!/usr/bin/env php'; $phar_begin_len = strlen( $phar_begin ); $bin_dir = getenv( 'WP_CLI_BIN_DIR' ); + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; if ( false !== $bin_dir && file_exists( $bin_dir . '/wp' ) && file_get_contents( $bin_dir . '/wp', false, null, 0, $phar_begin_len ) === $phar_begin ) { - $phar_path = $bin_dir . '/wp'; + $phar_path = $bin_dir . $bin; } else { $src_dir = dirname( __DIR__, 2 ); - $bin_path = $src_dir . '/bin/wp'; - $vendor_bin_path = $src_dir . '/vendor/bin/wp'; + $bin_path = $src_dir . '/bin/' . $bin; + $vendor_bin_path = $src_dir . '/vendor/bin/' . $bin; if ( file_exists( $bin_path ) && is_executable( $bin_path ) ) { $shell_path = $bin_path; } elseif ( file_exists( $vendor_bin_path ) && is_executable( $vendor_bin_path ) ) { $shell_path = $vendor_bin_path; } else { - $shell_path = 'wp'; + $shell_path = $bin; } } } From 3ad0ed88d9796b054a1a05646a789f9c77afed83 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:14:50 +0200 Subject: [PATCH 047/107] early return --- src/Context/FeatureContext.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index ae58a2789..790db9fb6 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -411,12 +411,11 @@ public static function get_bin_path(): ?string { foreach ( $bin_paths as $path ) { $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { - $bin_path = $path; - break; + return $path; } } - return $bin_path; + return null; } /** From 0a5ab90a9926877e6af3de1fca3c283525c3d42f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:19:43 +0200 Subject: [PATCH 048/107] Try Windows workaround for unit tests --- tests/tests/TestBehatTags.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 0e8be1040..2d2aa52b8 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -55,7 +55,14 @@ public function test_behat_tags_wp_version_github_token( $env, $expected ): void $contents = '@require-wp-4.6 @require-wp-4.8 @require-wp-4.9 @less-than-wp-4.6 @less-than-wp-4.8 @less-than-wp-4.9'; file_put_contents( $this->temp_dir . '/features/wp_version.feature', $contents ); - $output = exec( "cd {$this->temp_dir}; $env php $behat_tags" ); + if ( ! empty( $env ) ) { + putenv( $env ); + } + $output = exec( 'cd ' . escapeshellarg( $this->temp_dir ) . ' && php ' . escapeshellarg( $behat_tags ) ); + if ( ! empty( $env ) ) { + list( $key ) = explode( '=', $env, 2 ); + putenv( $key ); + } $expected .= '&&~@broken'; if ( in_array( $env, array( 'WP_VERSION=trunk', 'WP_VERSION=nightly' ), true ) ) { From 0f1f277c63f9b4abacebdd23759cdb45fa8d477d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:23:12 +0200 Subject: [PATCH 049/107] debugging --- src/Context/FeatureContext.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 790db9fb6..61a7162c2 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -408,12 +408,18 @@ public static function get_bin_path(): ?string { $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; + wp_cli_behat_env_debug( 'Searching for WP-CLI binary...' ); foreach ( $bin_paths as $path ) { $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; + wp_cli_behat_env_debug( "Checking path: {$full_bin_path}" ); + wp_cli_behat_env_debug( 'is_file: ' . ( is_file( $full_bin_path ) ? 'true' : 'false' ) ); + wp_cli_behat_env_debug( 'is_executable: ' . ( is_executable( $full_bin_path ) ? 'true' : 'false' ) ); if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { + wp_cli_behat_env_debug( "Found at: {$path}" ); return $path; } } + wp_cli_behat_env_debug( 'WP-CLI binary not found in search paths.' ); return null; } From a299ad2d2bc329a88543fb5d4212f22f8d141afc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:31:26 +0200 Subject: [PATCH 050/107] DRY unit test --- tests/tests/TestBehatTags.php | 45 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 2d2aa52b8..55d2cda15 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -35,6 +35,30 @@ protected function tear_down(): void { parent::tear_down(); } + /** + * Runs the behat-tags.php script in a cross-platform way. + * + * @param string $env Environment variable string to set (e.g., 'WP_VERSION=4.5'). + * @return string|false The output of the script. + */ + private function run_behat_tags_script( $env = '' ) { + $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + + if ( ! empty( $env ) ) { + putenv( $env ); + } + + $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && php ' . escapeshellarg( $behat_tags ); + $output = exec( $command ); + + if ( ! empty( $env ) ) { + list( $key ) = explode( '=', $env, 2 ); + putenv( $key ); // Unsets the variable. + } + + return $output; + } + /** * @dataProvider data_behat_tags_wp_version_github_token * @@ -50,19 +74,10 @@ public function test_behat_tags_wp_version_github_token( $env, $expected ): void putenv( 'WP_VERSION' ); putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - $contents = '@require-wp-4.6 @require-wp-4.8 @require-wp-4.9 @less-than-wp-4.6 @less-than-wp-4.8 @less-than-wp-4.9'; file_put_contents( $this->temp_dir . '/features/wp_version.feature', $contents ); - if ( ! empty( $env ) ) { - putenv( $env ); - } - $output = exec( 'cd ' . escapeshellarg( $this->temp_dir ) . ' && php ' . escapeshellarg( $behat_tags ) ); - if ( ! empty( $env ) ) { - list( $key ) = explode( '=', $env, 2 ); - putenv( $key ); - } + $output = $this->run_behat_tags_script( $env ); $expected .= '&&~@broken'; if ( in_array( $env, array( 'WP_VERSION=trunk', 'WP_VERSION=nightly' ), true ) ) { @@ -116,8 +131,6 @@ public function test_behat_tags_php_version(): void { putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - $php_version = substr( PHP_VERSION, 0, 3 ); $contents = ''; $expected = ''; @@ -172,7 +185,7 @@ public function test_behat_tags_php_version(): void { file_put_contents( $this->temp_dir . '/features/php_version.feature', $contents ); - $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); + $output = $this->run_behat_tags_script(); $this->assertSame( '--tags=' . $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); @@ -184,8 +197,6 @@ public function test_behat_tags_extension(): void { putenv( 'GITHUB_TOKEN' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - file_put_contents( $this->temp_dir . '/features/extension.feature', '@require-extension-imagick @require-extension-curl' ); $expecteds = array(); @@ -215,7 +226,7 @@ public function test_behat_tags_extension(): void { } $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); - $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); + $output = $this->run_behat_tags_script(); $this->assertSame( $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); @@ -264,7 +275,7 @@ public function test_behat_tags_db_version(): void { file_put_contents( $this->temp_dir . '/features/extension.feature', $contents ); $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); - $output = exec( "cd {$this->temp_dir}; php $behat_tags" ); + $output = $this->run_behat_tags_script(); $this->assertSame( $expected, $output ); } } From b4a0372216c0f3c398bc2b91bacfab684a0cfa34 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:32:01 +0200 Subject: [PATCH 051/107] No is_executable check on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SOmehow returns false even though `.bat` is supposed to always return true? 🤷 --- src/Context/FeatureContext.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 61a7162c2..4c0f204e5 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -408,18 +408,12 @@ public static function get_bin_path(): ?string { $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; - wp_cli_behat_env_debug( 'Searching for WP-CLI binary...' ); foreach ( $bin_paths as $path ) { $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; - wp_cli_behat_env_debug( "Checking path: {$full_bin_path}" ); - wp_cli_behat_env_debug( 'is_file: ' . ( is_file( $full_bin_path ) ? 'true' : 'false' ) ); - wp_cli_behat_env_debug( 'is_executable: ' . ( is_executable( $full_bin_path ) ? 'true' : 'false' ) ); - if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { - wp_cli_behat_env_debug( "Found at: {$path}" ); + if ( is_file( $full_bin_path ) && ( Utils\is_windows() || is_executable( $full_bin_path ) ) ) { return $path; } } - wp_cli_behat_env_debug( 'WP-CLI binary not found in search paths.' ); return null; } From 261480285f653389a80beb4d4d22fd83c360aabd Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:38:20 +0200 Subject: [PATCH 052/107] avoid using `grep` --- utils/behat-tags.php | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/utils/behat-tags.php b/utils/behat-tags.php index 4a8bad233..322191239 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -23,10 +23,17 @@ function version_tags( return array(); } - exec( - "grep '@{$prefix}-[0-9\.]*' -h -o {$features_folder}/*.feature | uniq", - $existing_tags - ); + $existing_tags = array(); + $feature_files = glob( $features_folder . '/*.feature' ); + if ( ! empty( $feature_files ) ) { + foreach ( $feature_files as $feature_file ) { + $contents = file_get_contents( $feature_file ); + if ( preg_match_all( '/@' . $prefix . '-[0-9\.]+/', $contents, $matches ) ) { + $existing_tags = array_merge( $existing_tags, $matches[0] ); + } + } + $existing_tags = array_unique( $existing_tags ); + } $skip_tags = array(); @@ -41,7 +48,11 @@ function version_tags( } function get_db_version() { - $version_string = exec( getenv( 'WP_CLI_TEST_DBTYPE' ) === 'mariadb' ? 'mariadb --version' : 'mysql -V' ); + $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); + if ( 'sqlite' === $db_type ) { + return ''; + } + $version_string = exec( 'mariadb' === $db_type ? 'mariadb --version' : 'mysql -V' ); preg_match( '@[0-9]+\.[0-9]+\.[0-9]+@', $version_string, $version ); return $version[0]; } @@ -116,10 +127,16 @@ function get_db_version() { # Require PHP extension, eg 'imagick'. function extension_tags( $features_folder = 'features' ) { $extension_tags = array(); - exec( - "grep '@require-extension-[A-Za-z_]*' -h -o {$features_folder}/*.feature | uniq", - $extension_tags - ); + $feature_files = glob( $features_folder . '/*.feature' ); + if ( ! empty( $feature_files ) ) { + foreach ( $feature_files as $feature_file ) { + $contents = file_get_contents( $feature_file ); + if ( preg_match_all( '/@require-extension-[A-Za-z_]*/', $contents, $matches ) ) { + $extension_tags = array_merge( $extension_tags, $matches[0] ); + } + } + $extension_tags = array_unique( $extension_tags ); + } $skip_tags = array(); From c1be92787950e25d72c16c700dd6b8260cc66fa9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:46:47 +0200 Subject: [PATCH 053/107] more debugging --- src/Context/FeatureContext.php | 80 +++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 25 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 4c0f204e5..b3e4d8824 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -406,12 +406,26 @@ public static function get_bin_path(): ?string { self::get_framework_dir() . DIRECTORY_SEPARATOR . 'bin', ]; - $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; - - foreach ( $bin_paths as $path ) { - $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; - if ( is_file( $full_bin_path ) && ( Utils\is_windows() || is_executable( $full_bin_path ) ) ) { - return $path; + if ( Utils\is_windows() ) { + foreach ( $bin_paths as $path ) { + $wp_script_path = $path . DIRECTORY_SEPARATOR . 'wp'; + if ( is_file( $wp_script_path ) ) { + $wp_bat_path = $path . DIRECTORY_SEPARATOR . 'wp.bat'; + if ( ! is_file( $wp_bat_path ) ) { + $bat_content = '@ECHO OFF' . PHP_EOL; + // Use the currently running PHP executable to avoid PATH issues. + $bat_content .= '"' . PHP_BINARY . '" "' . realpath( $wp_script_path ) . '" %*'; + file_put_contents( $wp_bat_path, $bat_content ); + } + return $path; + } + } + } else { + foreach ( $bin_paths as $path ) { + $full_bin_path = $path . DIRECTORY_SEPARATOR . 'wp'; + if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { + return $path; + } } } @@ -439,19 +453,10 @@ private static function get_process_env_variables(): array { wp_cli_behat_env_debug( "WP-CLI binary path: {$bin_path}" ); - $bin = $bin_path . DIRECTORY_SEPARATOR . ( Utils\is_windows() ? 'wp.bat' : 'wp' ); - - if ( ! file_exists( $bin ) ) { - wp_cli_behat_env_debug( "WARNING: File $bin not found." ); - } - - if ( ! is_executable( $bin ) ) { - wp_cli_behat_env_debug( "WARNING: File $bin is not executable." ); - } - - $path_separator = Utils\is_windows() ? ';' : ':'; - $env = [ - 'PATH' => $bin_path . $path_separator . getenv( 'PATH' ), + $path_separator = Utils\is_windows() ? ';' : ':'; + $php_binary_path = dirname( PHP_BINARY ); + $env = [ + 'PATH' => $php_binary_path . $path_separator . $bin_path . $path_separator . getenv( 'PATH' ), 'BEHAT_RUN' => 1, 'HOME' => sys_get_temp_dir() . '/wp-cli-home', 'TEST_RUN_DIR' => self::$behat_run_dir, @@ -1241,6 +1246,10 @@ public function proc( $command, $assoc_args = [], $path = '' ): Process { $cwd = null; } + wp_cli_behat_env_debug( "Running command: {$command}" ); + wp_cli_behat_env_debug( "In directory: {$cwd}" ); + wp_cli_behat_env_debug( "With PATH: {$env['PATH']}" ); + return Process::create( $command, $cwd, $env ); } @@ -1250,20 +1259,41 @@ public function proc( $command, $assoc_args = [], $path = '' ): Process { * @param string $cmd */ public function background_proc( $cmd ): void { - $descriptors = [ - 0 => STDIN, - 1 => [ 'pipe', 'w' ], - 2 => [ 'pipe', 'w' ], - ]; + if ( Utils\is_windows() ) { + // On Windows, leaving pipes open can cause hangs. + // Redirect output to files and close stdin. + $stdout_file = tempnam( sys_get_temp_dir(), 'behat-stdout-' ); + $stderr_file = tempnam( sys_get_temp_dir(), 'behat-stderr-' ); + $descriptors = [ + 0 => [ 'pipe', 'r' ], + 1 => [ 'file', $stdout_file, 'a' ], + 2 => [ 'file', $stderr_file, 'a' ], + ]; + } else { + $descriptors = [ + 0 => STDIN, + 1 => [ 'pipe', 'w' ], + 2 => [ 'pipe', 'w' ], + ]; + } $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); + if ( Utils\is_windows() ) { + fclose( $pipes[0] ); + } + sleep( 1 ); $status = proc_get_status( $proc ); if ( ! $status['running'] ) { - $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; + if ( Utils\is_windows() ) { + $stderr = file_get_contents( $stderr_file ); + $stderr = $stderr ? ': ' . $stderr : ''; + } else { + $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; + } throw new RuntimeException( sprintf( "Failed to start background process '%s'%s.", $cmd, $stderr ) ); } From 8182330f10f8fab74c61d97c463fcab6b71d3700 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 13:55:46 +0200 Subject: [PATCH 054/107] More AI fixes --- tests/tests/TestBehatTags.php | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 55d2cda15..fd73da0e9 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -44,18 +44,25 @@ protected function tear_down(): void { private function run_behat_tags_script( $env = '' ) { $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + // Use the same PHP binary that is running the tests to ensure extension consistency. + $php_run = escapeshellarg( PHP_BINARY ) . ' ' . escapeshellarg( $behat_tags ); + + $command = ''; if ( ! empty( $env ) ) { - putenv( $env ); + // putenv() can be unreliable, especially on Windows. + // Prepending the variable to the command is a more robust cross-platform solution. + if ( Utils\is_windows() ) { + // Note: `set` is internal to `cmd.exe` and works on the subsequent command after `&&`. + $command = 'set ' . $env . ' && '; + } else { + // On Unix-like systems, this sets the variable for the duration of the command. + $command = $env . ' '; + } } - $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && php ' . escapeshellarg( $behat_tags ); + $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . $command . $php_run; $output = exec( $command ); - if ( ! empty( $env ) ) { - list( $key ) = explode( '=', $env, 2 ); - putenv( $key ); // Unsets the variable. - } - return $output; } From 4e1559b61e10ca7407659cc5e1c9b478e0a1d2ec Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 14:00:06 +0200 Subject: [PATCH 055/107] ai debugging --- src/Context/FeatureContext.php | 13 ++++++++----- tests/tests/TestBehatTags.php | 12 ++++++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index b3e4d8824..f5238a6fd 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -675,7 +675,11 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::$mysql_binary = Utils\get_mysql_binary_path(); } - $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); + $command = 'wp cli info'; + if ( Utils\is_windows() ) { + $command .= ' > NUL 2>&1'; + } + $result = Process::create( $command, null, self::get_process_env_variables() )->run_check(); echo "{$result->stdout}\n"; // Remove install cache if any (not setting the static var). @@ -685,10 +689,6 @@ public static function prepare( BeforeSuiteScope $scope ): void { if ( file_exists( $install_cache_dir ) ) { self::remove_dir( $install_cache_dir ); } - - if ( getenv( 'WP_CLI_TEST_DEBUG_BEHAT_ENV' ) ) { - exit; - } } /** @@ -1854,6 +1854,9 @@ private static function log_proc_method_run_time( $key, $start_time ): void { * @param string $message */ function wp_cli_behat_env_debug( $message ): void { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed + // Always print the message to STDERR for debugging purposes. + fwrite( STDERR, "{$message}\n" ); + if ( ! getenv( 'WP_CLI_TEST_DEBUG_BEHAT_ENV' ) ) { return; } diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index fd73da0e9..882595414 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -44,16 +44,20 @@ protected function tear_down(): void { private function run_behat_tags_script( $env = '' ) { $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - // Use the same PHP binary that is running the tests to ensure extension consistency. - $php_run = escapeshellarg( PHP_BINARY ) . ' ' . escapeshellarg( $behat_tags ); + // Get the loaded ini file from the current process to ensure config consistency. + $ini_file = php_ini_loaded_file(); + $ini_arg = $ini_file ? ' -c ' . escapeshellarg( $ini_file ) : ''; + + // Use the same PHP binary that is running the tests. + $php_run = escapeshellarg( PHP_BINARY ) . $ini_arg . ' ' . escapeshellarg( $behat_tags ); $command = ''; if ( ! empty( $env ) ) { // putenv() can be unreliable, especially on Windows. // Prepending the variable to the command is a more robust cross-platform solution. - if ( Utils\is_windows() ) { + if ( DIRECTORY_SEPARATOR === '\\' ) { // Windows // Note: `set` is internal to `cmd.exe` and works on the subsequent command after `&&`. - $command = 'set ' . $env . ' && '; + $command = 'set ' . escapeshellarg( $env ) . ' && '; } else { // On Unix-like systems, this sets the variable for the duration of the command. $command = $env . ' '; From dc0b0050fad3d11ef3f8cb0f38800cbaf835001d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 14:14:29 +0200 Subject: [PATCH 056/107] use modified process class --- src/Context/FeatureContext.php | 8 +- src/Context/Process.php | 174 +++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 src/Context/Process.php diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index f5238a6fd..92f97ff84 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -23,7 +23,6 @@ use RuntimeException; use WP_CLI; use DirectoryIterator; -use WP_CLI\Process; use WP_CLI\ProcessRun; use WP_CLI\Utils; use WP_CLI\WpOrgApi; @@ -675,11 +674,7 @@ public static function prepare( BeforeSuiteScope $scope ): void { self::$mysql_binary = Utils\get_mysql_binary_path(); } - $command = 'wp cli info'; - if ( Utils\is_windows() ) { - $command .= ' > NUL 2>&1'; - } - $result = Process::create( $command, null, self::get_process_env_variables() )->run_check(); + $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); echo "{$result->stdout}\n"; // Remove install cache if any (not setting the static var). @@ -1884,7 +1879,6 @@ function wpcli_bootstrap_behat_feature_context(): void { // Load helper functionality that is needed for the tests. require_once "{$framework_folder}/php/utils.php"; - require_once "{$framework_folder}/php/WP_CLI/Process.php"; require_once "{$framework_folder}/php/WP_CLI/ProcessRun.php"; // Manually load Composer file includes by generating a config with require: diff --git a/src/Context/Process.php b/src/Context/Process.php new file mode 100644 index 000000000..a83b851c7 --- /dev/null +++ b/src/Context/Process.php @@ -0,0 +1,174 @@ + STDIN, + 1 => [ 'pipe', 'w' ], + 2 => [ 'pipe', 'w' ], + ]; + + /** + * @var bool Whether to log run time info or not. + */ + public static $log_run_times = false; + + /** + * @var array Array of process run time info, keyed by process command, each a 2-element array containing run time and run count. + */ + public static $run_times = []; + + /** + * @param string $command Command to execute. + * @param string|null $cwd Directory to execute the command in. + * @param array|null $env Environment variables to set when running the command. + * + * @return Process + */ + public static function create( $command, $cwd = null, $env = [] ) { + $proc = new self(); + + $proc->command = $command; + $proc->cwd = $cwd; + $proc->env = $env; + + return $proc; + } + + private function __construct() {} + + /** + * Run the command. + * + * @return \WP_CLI\ProcessRun + */ + public function run() { + \WP_CLI\Utils\check_proc_available( 'Process::run' ); + + $start_time = microtime( true ); + + $pipes = []; + if ( \WP_CLI\Utils\is_windows() ) { + // On Windows, leaving pipes open can cause hangs. + // Redirect output to files and close stdin. + $stdout_file = tempnam( sys_get_temp_dir(), 'behat-stdout-' ); + $stderr_file = tempnam( sys_get_temp_dir(), 'behat-stderr-' ); + $descriptors = [ + 0 => [ 'pipe', 'r' ], + 1 => [ 'file', $stdout_file, 'a' ], + 2 => [ 'file', $stderr_file, 'a' ], + ]; + $proc = \WP_CLI\Utils\proc_open_compat( $this->command, $descriptors, $pipes, $this->cwd, $this->env ); + fclose( $pipes[0] ); + $stdout = file_get_contents( $stdout_file ); + $stderr = file_get_contents( $stderr_file ); + unlink( $stdout_file ); + unlink( $stderr_file ); + } else { + $proc = \WP_CLI\Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); + $stdout = stream_get_contents( $pipes[1] ); + fclose( $pipes[1] ); + $stderr = stream_get_contents( $pipes[2] ); + fclose( $pipes[2] ); + } + + $return_code = proc_close( $proc ); + + $run_time = microtime( true ) - $start_time; + + if ( self::$log_run_times ) { + if ( ! isset( self::$run_times[ $this->command ] ) ) { + self::$run_times[ $this->command ] = [ 0, 0 ]; + } + self::$run_times[ $this->command ][0] += $run_time; + ++self::$run_times[ $this->command ][1]; + } + + return new \WP_CLI\ProcessRun( + [ + 'stdout' => $stdout, + 'stderr' => $stderr, + 'return_code' => $return_code, + 'command' => $this->command, + 'cwd' => $this->cwd, + 'env' => $this->env, + 'run_time' => $run_time, + ] + ); + } + + /** + * Run the command, but throw an Exception on error. + * + * @return \WP_CLI\ProcessRun + */ + public function run_check() { + $r = $this->run(); + + if ( $r->return_code ) { + throw new RuntimeException( $r ); + } + + return $r; + } + + /** + * Run the command, but throw an Exception on error. + * Same as `run_check()` above, but checks the correct stderr. + * + * @return \WP_CLI\ProcessRun + */ + public function run_check_stderr() { + $r = $this->run(); + + if ( $r->return_code ) { + throw new RuntimeException( $r ); + } + + if ( ! empty( $r->stderr ) ) { + // If the only thing that STDERR caught was the Requests deprecated message, ignore it. + // This is a temporary fix until we have a better solution for dealing with Requests + // as a dependency shared between WP Core and WP-CLI. + $stderr_lines = array_filter( explode( "\n", $r->stderr ) ); + if ( 1 === count( $stderr_lines ) ) { + $stderr_line = $stderr_lines[0]; + if ( + false !== strpos( + $stderr_line, + 'The PSR-0 `Requests_...` class names in the Request library are deprecated.' + ) + ) { + return $r; + } + } + + throw new RuntimeException( $r ); + } + + return $r; + } +} From bc16f04164f40cfcd39fd1371471b01a9cee74bf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 14:14:52 +0200 Subject: [PATCH 057/107] dbg --- tests/tests/TestBehatTags.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 882595414..497940b17 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -53,11 +53,11 @@ private function run_behat_tags_script( $env = '' ) { $command = ''; if ( ! empty( $env ) ) { - // putenv() can be unreliable, especially on Windows. - // Prepending the variable to the command is a more robust cross-platform solution. + // putenv() can be unreliable. Prepending the variable to the command is more robust. if ( DIRECTORY_SEPARATOR === '\\' ) { // Windows - // Note: `set` is internal to `cmd.exe` and works on the subsequent command after `&&`. - $command = 'set ' . escapeshellarg( $env ) . ' && '; + // `set` is internal to `cmd.exe`. Do not escape the $env variable, as it's from a trusted + // data provider and `escapeshellarg` adds quotes that `set` doesn't understand. + $command = 'set ' . $env . ' && '; } else { // On Unix-like systems, this sets the variable for the duration of the command. $command = $env . ' '; @@ -65,6 +65,7 @@ private function run_behat_tags_script( $env = '' ) { } $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . $command . $php_run; + echo "Executing Command: {$command}\n"; $output = exec( $command ); return $output; @@ -238,6 +239,9 @@ public function test_behat_tags_extension(): void { $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); $output = $this->run_behat_tags_script(); + echo "Imagick Loaded in Test: " . ( extension_loaded( 'imagick' ) ? 'Yes' : 'No' ) . "\n"; + echo "Expected: {$expected}\n"; + echo "Actual: {$output}\n"; $this->assertSame( $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); From 6f1fdab42f9b5b79cea9c6e1cd2b323387b9f747 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 14:23:23 +0200 Subject: [PATCH 058/107] AI-suggested fix --- tests/tests/TestBehatTags.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 497940b17..4413be85e 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -44,12 +44,8 @@ protected function tear_down(): void { private function run_behat_tags_script( $env = '' ) { $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; - // Get the loaded ini file from the current process to ensure config consistency. - $ini_file = php_ini_loaded_file(); - $ini_arg = $ini_file ? ' -c ' . escapeshellarg( $ini_file ) : ''; - - // Use the same PHP binary that is running the tests. - $php_run = escapeshellarg( PHP_BINARY ) . $ini_arg . ' ' . escapeshellarg( $behat_tags ); + // Use the `-n` flag to disable loading of `php.ini` and ensure a clean environment. + $php_run = escapeshellarg( PHP_BINARY ) . ' -n ' . escapeshellarg( $behat_tags ); $command = ''; if ( ! empty( $env ) ) { @@ -65,7 +61,6 @@ private function run_behat_tags_script( $env = '' ) { } $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . $command . $php_run; - echo "Executing Command: {$command}\n"; $output = exec( $command ); return $output; @@ -239,9 +234,6 @@ public function test_behat_tags_extension(): void { $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); $output = $this->run_behat_tags_script(); - echo "Imagick Loaded in Test: " . ( extension_loaded( 'imagick' ) ? 'Yes' : 'No' ) . "\n"; - echo "Expected: {$expected}\n"; - echo "Actual: {$output}\n"; $this->assertSame( $expected, $output ); putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); From 8da6b94a677f2b79681267d6c859f3a8008b2474 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 14:24:14 +0200 Subject: [PATCH 059/107] sub-process fixes --- src/Context/FeatureContext.php | 2 -- src/Context/Process.php | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 92f97ff84..7a5b699f1 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1242,8 +1242,6 @@ public function proc( $command, $assoc_args = [], $path = '' ): Process { } wp_cli_behat_env_debug( "Running command: {$command}" ); - wp_cli_behat_env_debug( "In directory: {$cwd}" ); - wp_cli_behat_env_debug( "With PATH: {$env['PATH']}" ); return Process::create( $command, $cwd, $env ); } diff --git a/src/Context/Process.php b/src/Context/Process.php index a83b851c7..30da1ccfe 100644 --- a/src/Context/Process.php +++ b/src/Context/Process.php @@ -84,10 +84,6 @@ public function run() { ]; $proc = \WP_CLI\Utils\proc_open_compat( $this->command, $descriptors, $pipes, $this->cwd, $this->env ); fclose( $pipes[0] ); - $stdout = file_get_contents( $stdout_file ); - $stderr = file_get_contents( $stderr_file ); - unlink( $stdout_file ); - unlink( $stderr_file ); } else { $proc = \WP_CLI\Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); $stdout = stream_get_contents( $pipes[1] ); @@ -98,6 +94,13 @@ public function run() { $return_code = proc_close( $proc ); + if ( \WP_CLI\Utils\is_windows() ) { + $stdout = file_get_contents( $stdout_file ); + $stderr = file_get_contents( $stderr_file ); + unlink( $stdout_file ); + unlink( $stderr_file ); + } + $run_time = microtime( true ) - $start_time; if ( self::$log_run_times ) { From dc3296a0fe8467f3d308f9ff0886b45a308128c6 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 14:27:48 +0200 Subject: [PATCH 060/107] AI fixes --- src/Context/FeatureContext.php | 33 ++++++++++----------------------- src/Context/Process.php | 2 +- tests/tests/TestBehatTags.php | 7 +++++-- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 7a5b699f1..b1fb77c2e 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -405,26 +405,12 @@ public static function get_bin_path(): ?string { self::get_framework_dir() . DIRECTORY_SEPARATOR . 'bin', ]; - if ( Utils\is_windows() ) { - foreach ( $bin_paths as $path ) { - $wp_script_path = $path . DIRECTORY_SEPARATOR . 'wp'; - if ( is_file( $wp_script_path ) ) { - $wp_bat_path = $path . DIRECTORY_SEPARATOR . 'wp.bat'; - if ( ! is_file( $wp_bat_path ) ) { - $bat_content = '@ECHO OFF' . PHP_EOL; - // Use the currently running PHP executable to avoid PATH issues. - $bat_content .= '"' . PHP_BINARY . '" "' . realpath( $wp_script_path ) . '" %*'; - file_put_contents( $wp_bat_path, $bat_content ); - } - return $path; - } - } - } else { - foreach ( $bin_paths as $path ) { - $full_bin_path = $path . DIRECTORY_SEPARATOR . 'wp'; - if ( is_file( $full_bin_path ) && is_executable( $full_bin_path ) ) { - return $path; - } + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; + + foreach ( $bin_paths as $path ) { + $full_bin_path = $path . DIRECTORY_SEPARATOR . $bin; + if ( is_file( $full_bin_path ) && ( Utils\is_windows() || is_executable( $full_bin_path ) ) ) { + return $path; } } @@ -684,6 +670,10 @@ public static function prepare( BeforeSuiteScope $scope ): void { if ( file_exists( $install_cache_dir ) ) { self::remove_dir( $install_cache_dir ); } + + if ( getenv( 'WP_CLI_TEST_DEBUG_BEHAT_ENV' ) ) { + exit; + } } /** @@ -1847,9 +1837,6 @@ private static function log_proc_method_run_time( $key, $start_time ): void { * @param string $message */ function wp_cli_behat_env_debug( $message ): void { // phpcs:ignore Universal.Files.SeparateFunctionsFromOO.Mixed - // Always print the message to STDERR for debugging purposes. - fwrite( STDERR, "{$message}\n" ); - if ( ! getenv( 'WP_CLI_TEST_DEBUG_BEHAT_ENV' ) ) { return; } diff --git a/src/Context/Process.php b/src/Context/Process.php index 30da1ccfe..ff9184e61 100644 --- a/src/Context/Process.php +++ b/src/Context/Process.php @@ -82,7 +82,7 @@ public function run() { 1 => [ 'file', $stdout_file, 'a' ], 2 => [ 'file', $stderr_file, 'a' ], ]; - $proc = \WP_CLI\Utils\proc_open_compat( $this->command, $descriptors, $pipes, $this->cwd, $this->env ); + $proc = \WP_CLI\Utils\proc_open_compat( $this->command, $descriptors, $pipes, $this->cwd, $this->env ); fclose( $pipes[0] ); } else { $proc = \WP_CLI\Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 4413be85e..ebc6fe099 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -225,10 +225,13 @@ public function test_behat_tags_extension(): void { break; } - if ( ! extension_loaded( 'imagick' ) ) { + // Check which extensions are loaded in the clean `php -n` environment to build the correct expectation. + $imagick_loaded_in_script = (bool) exec( escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'imagick\');"' ); + if ( ! $imagick_loaded_in_script ) { $expecteds[] = '~@require-extension-imagick'; } - if ( ! extension_loaded( 'curl' ) ) { + $curl_loaded_in_script = (bool) exec( escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'curl\');"' ); + if ( ! $curl_loaded_in_script ) { $expecteds[] = '~@require-extension-curl'; } From 31285c801959e4b9a97ca29730e5314602ffb236 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 15:28:09 +0200 Subject: [PATCH 061/107] adjust regex --- features/steps.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps.feature b/features/steps.feature index 6968ba636..10fcf7384 100644 --- a/features/steps.feature +++ b/features/steps.feature @@ -61,7 +61,7 @@ Feature: Make sure "Given", "When", "Then" steps work as expected Scenario: Special variables When I run `echo {INVOKE_WP_CLI_WITH_PHP_ARGS-} cli info` - Then STDOUT should match /wp cli info/ + Then STDOUT should match /(wp|wp\.bat) cli info/ And STDERR should be empty When I run `echo {WP_VERSION-latest}` From 255fab4df0b70e028aba9ece12705e7dfc8155ec Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 15:38:41 +0200 Subject: [PATCH 062/107] Use predetermined mysql binary --- src/Context/FeatureContext.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index b1fb77c2e..c611fcecd 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -702,10 +702,9 @@ public function beforeScenario( BeforeScenarioScope $scope ): void { self::get_behat_internal_variables() ); - $mysql_binary = Utils\get_mysql_binary_path(); $sql_dump_command = Utils\get_sql_dump_command(); - $this->variables['MYSQL_BINARY'] = $mysql_binary; + $this->variables['MYSQL_BINARY'] = self::$mysql_binary; $this->variables['SQL_DUMP_COMMAND'] = $sql_dump_command; // Used in the names of the RUN_DIR and SUITE_CACHE_DIR directories. From 8b8f25e69a390492f10f09057ec9d4e821d0daad Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 15:38:56 +0200 Subject: [PATCH 063/107] Normalize line endings not sure if really needed --- src/Context/Process.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Context/Process.php b/src/Context/Process.php index ff9184e61..12d845513 100644 --- a/src/Context/Process.php +++ b/src/Context/Process.php @@ -99,6 +99,10 @@ public function run() { $stderr = file_get_contents( $stderr_file ); unlink( $stdout_file ); unlink( $stderr_file ); + + // Normalize line endings. + $stdout = str_replace( "\r\n", "\n", $stdout ); + $stderr = str_replace( "\r\n", "\n", $stderr ); } $run_time = microtime( true ) - $start_time; From 6ce7eefd5802d97861377d9693a049be00789b49 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 15:52:23 +0200 Subject: [PATCH 064/107] Try behat fixes --- src/Context/FeatureContext.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index c611fcecd..b529f4974 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -441,12 +441,15 @@ private static function get_process_env_variables(): array { $path_separator = Utils\is_windows() ? ';' : ':'; $php_binary_path = dirname( PHP_BINARY ); $env = [ - 'PATH' => $php_binary_path . $path_separator . $bin_path . $path_separator . getenv( 'PATH' ), - 'BEHAT_RUN' => 1, - 'HOME' => sys_get_temp_dir() . '/wp-cli-home', - 'TEST_RUN_DIR' => self::$behat_run_dir, + 'PATH' => $php_binary_path . $path_separator . $bin_path . $path_separator . getenv( 'PATH' ), + 'BEHAT_RUN' => 1, + 'HOME' => sys_get_temp_dir() . '/wp-cli-home', + 'COMPOSER_HOME' => sys_get_temp_dir() . '/wp-cli-composer-home', + 'TEST_RUN_DIR' => self::$behat_run_dir, ]; + $env = array_merge( $_ENV, $env ); + if ( self::running_with_code_coverage() ) { $has_coverage_driver = ( new Runtime() )->hasXdebug() || ( new Runtime() )->hasPCOV(); @@ -1441,7 +1444,7 @@ public function install_wp( $subdir = '' ): void { $subdir = $this->replace_variables( $subdir ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; + $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }" . PHP_EOL; if ( 'sqlite' !== self::$db_type ) { $this->create_db(); @@ -1517,9 +1520,9 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { $this->composer_command( 'require johnpbloch/wordpress-core-installer johnpbloch/wordpress-core --optimize-autoloader' ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; + $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }" . PHP_EOL; - $config_extra_php .= "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';\n"; + $config_extra_php .= "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';" . PHP_EOL; $this->create_config( 'WordPress', $config_extra_php ); From 530491f9066372730acc684f23ee5a97a10aeef5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 17:32:43 +0200 Subject: [PATCH 065/107] another phpunit attempt --- tests/tests/TestBehatTags.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index ebc6fe099..66f6299ec 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -53,7 +53,8 @@ private function run_behat_tags_script( $env = '' ) { if ( DIRECTORY_SEPARATOR === '\\' ) { // Windows // `set` is internal to `cmd.exe`. Do not escape the $env variable, as it's from a trusted // data provider and `escapeshellarg` adds quotes that `set` doesn't understand. - $command = 'set ' . $env . ' && '; + // Note: `set "VAR=VALUE"` is more robust than `set VAR=VALUE`. + $command = 'set "' . $env . '" && '; } else { // On Unix-like systems, this sets the variable for the duration of the command. $command = $env . ' '; @@ -226,11 +227,11 @@ public function test_behat_tags_extension(): void { } // Check which extensions are loaded in the clean `php -n` environment to build the correct expectation. - $imagick_loaded_in_script = (bool) exec( escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'imagick\');"' ); + $imagick_loaded_in_script = (bool) exec( 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'imagick\');"' ); if ( ! $imagick_loaded_in_script ) { $expecteds[] = '~@require-extension-imagick'; } - $curl_loaded_in_script = (bool) exec( escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'curl\');"' ); + $curl_loaded_in_script = (bool) exec( 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . escapeshellarg( PHP_BINARY ) . ' -n -r "echo (int)extension_loaded(\'curl\');"' ); if ( ! $curl_loaded_in_script ) { $expecteds[] = '~@require-extension-curl'; } From 785372bfd129865202a4f00d4c226c6ebc085c47 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 17:33:55 +0200 Subject: [PATCH 066/107] undo newline change --- src/Context/FeatureContext.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index b529f4974..b76d60198 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1444,7 +1444,7 @@ public function install_wp( $subdir = '' ): void { $subdir = $this->replace_variables( $subdir ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }" . PHP_EOL; + $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; if ( 'sqlite' !== self::$db_type ) { $this->create_db(); @@ -1520,9 +1520,9 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { $this->composer_command( 'require johnpbloch/wordpress-core-installer johnpbloch/wordpress-core --optimize-autoloader' ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }" . PHP_EOL; + $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; - $config_extra_php .= "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';" . PHP_EOL; + $config_extra_php .= "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';\n"; $this->create_config( 'WordPress', $config_extra_php ); From c5ad367b9c82dcec43552289d7939e937ea9c7b2 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 19:43:51 +0200 Subject: [PATCH 067/107] dir sep --- utils/behat-tags.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/behat-tags.php b/utils/behat-tags.php index 322191239..c11c49d57 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -24,7 +24,7 @@ function version_tags( } $existing_tags = array(); - $feature_files = glob( $features_folder . '/*.feature' ); + $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); if ( ! empty( $feature_files ) ) { foreach ( $feature_files as $feature_file ) { $contents = file_get_contents( $feature_file ); @@ -127,7 +127,7 @@ function get_db_version() { # Require PHP extension, eg 'imagick'. function extension_tags( $features_folder = 'features' ) { $extension_tags = array(); - $feature_files = glob( $features_folder . '/*.feature' ); + $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); if ( ! empty( $feature_files ) ) { foreach ( $feature_files as $feature_file ) { $contents = file_get_contents( $feature_file ); From fa97aa6bc9a49ee7bd56011c80f81e75c2550504 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 19:43:56 +0200 Subject: [PATCH 068/107] recursive remove dir --- tests/tests/TestBehatTags.php | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 66f6299ec..b518a66d2 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -23,18 +23,39 @@ protected function set_up(): void { } protected function tear_down(): void { - if ( $this->temp_dir && file_exists( $this->temp_dir ) ) { - foreach ( glob( $this->temp_dir . '/features/*' ) as $feature_file ) { - unlink( $feature_file ); - } - rmdir( $this->temp_dir . '/features' ); - rmdir( $this->temp_dir ); + $this->remove_dir( $this->temp_dir ); } parent::tear_down(); } + /** + * Recursively removes a directory and its contents. + * + * @param string $dir The directory to remove. + */ + private function remove_dir( $dir ) { + if ( ! is_dir( $dir ) ) { + return; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::SKIP_DOTS ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ( $iterator as $file ) { + if ( $file->isDir() ) { + rmdir( $file->getRealPath() ); + } else { + unlink( $file->getRealPath() ); + } + } + + rmdir( $dir ); + } + /** * Runs the behat-tags.php script in a cross-platform way. * From 1051290409d4744ec8ffdfb35cbd51cbef36917f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 19:46:20 +0200 Subject: [PATCH 069/107] debug step --- features/testing.feature | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/features/testing.feature b/features/testing.feature index 4bee962c4..30bff5b80 100644 --- a/features/testing.feature +++ b/features/testing.feature @@ -11,6 +11,10 @@ Feature: Test that WP-CLI loads. Scenario: WP Cron is disabled by default Given a WP install + And the wp-config.php file should contain: + """ + if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); } + """ And a test_cron.php file: """ Date: Sun, 12 Oct 2025 19:56:22 +0200 Subject: [PATCH 070/107] More `DIRECTORY_SEPARATOR` --- tests/tests/TestBehatTags.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index b518a66d2..3d9536269 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -19,7 +19,7 @@ protected function set_up(): void { $this->temp_dir = Utils\get_temp_dir() . uniqid( 'wp-cli-test-behat-tags-', true ); mkdir( $this->temp_dir ); - mkdir( $this->temp_dir . '/features' ); + mkdir( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' ); } protected function tear_down(): void { @@ -71,7 +71,7 @@ private function run_behat_tags_script( $env = '' ) { $command = ''; if ( ! empty( $env ) ) { // putenv() can be unreliable. Prepending the variable to the command is more robust. - if ( DIRECTORY_SEPARATOR === '\\' ) { // Windows + if ( Utils\is_windows() ) { // `set` is internal to `cmd.exe`. Do not escape the $env variable, as it's from a trusted // data provider and `escapeshellarg` adds quotes that `set` doesn't understand. // Note: `set "VAR=VALUE"` is more robust than `set VAR=VALUE`. @@ -104,7 +104,7 @@ public function test_behat_tags_wp_version_github_token( $env, $expected ): void putenv( 'GITHUB_TOKEN' ); $contents = '@require-wp-4.6 @require-wp-4.8 @require-wp-4.9 @less-than-wp-4.6 @less-than-wp-4.8 @less-than-wp-4.9'; - file_put_contents( $this->temp_dir . '/features/wp_version.feature', $contents ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'wp_version.feature', $contents ); $output = $this->run_behat_tags_script( $env ); @@ -212,7 +212,7 @@ public function test_behat_tags_php_version(): void { break; } - file_put_contents( $this->temp_dir . '/features/php_version.feature', $contents ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'php_version.feature', $contents ); $output = $this->run_behat_tags_script(); $this->assertSame( '--tags=' . $expected, $output ); @@ -226,7 +226,7 @@ public function test_behat_tags_extension(): void { putenv( 'GITHUB_TOKEN' ); - file_put_contents( $this->temp_dir . '/features/extension.feature', '@require-extension-imagick @require-extension-curl' ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'extension.feature', '@require-extension-imagick @require-extension-curl' ); $expecteds = array(); @@ -304,7 +304,7 @@ public function test_behat_tags_db_version(): void { break; } - file_put_contents( $this->temp_dir . '/features/extension.feature', $contents ); + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'extension.feature', $contents ); $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); $output = $this->run_behat_tags_script(); From ad84c65cd84a0dec405ccd2d3b12ebb45aee593d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 19:57:25 +0200 Subject: [PATCH 071/107] Avoid using exclamation mark --- src/Context/FeatureContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index b76d60198..8a69c31de 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1444,7 +1444,7 @@ public function install_wp( $subdir = '' ): void { $subdir = $this->replace_variables( $subdir ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; + $config_extra_php = "if ( defined( 'DISABLE_WP_CRON' ) === false ) { define( 'DISABLE_WP_CRON', true ); }\n"; if ( 'sqlite' !== self::$db_type ) { $this->create_db(); @@ -1520,7 +1520,7 @@ public function install_wp_with_composer( $vendor_directory = 'vendor' ): void { $this->composer_command( 'require johnpbloch/wordpress-core-installer johnpbloch/wordpress-core --optimize-autoloader' ); // Disable WP Cron by default to avoid bogus HTTP requests in CLI context. - $config_extra_php = "if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); }\n"; + $config_extra_php = "if ( defined( 'DISABLE_WP_CRON' ) === false ) { define( 'DISABLE_WP_CRON', true ); }\n"; $config_extra_php .= "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';\n"; From ae4aff8728fbfffdec435f680d8a0c73bd656e24 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:02:46 +0200 Subject: [PATCH 072/107] Update assertion --- features/testing.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/testing.feature b/features/testing.feature index 30bff5b80..48e714062 100644 --- a/features/testing.feature +++ b/features/testing.feature @@ -13,7 +13,7 @@ Feature: Test that WP-CLI loads. Given a WP install And the wp-config.php file should contain: """ - if ( ! defined( 'DISABLE_WP_CRON' ) ) { define( 'DISABLE_WP_CRON', true ); } + if ( defined( 'DISABLE_WP_CRON' ) === false ) { define( 'DISABLE_WP_CRON', true ); } """ And a test_cron.php file: """ From b7b0b5c6d3093f59c0f28ff249c153b4832e236e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:06:00 +0200 Subject: [PATCH 073/107] dir sep --- tests/tests/TestBehatTags.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 3d9536269..a84b465ac 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -63,7 +63,7 @@ private function remove_dir( $dir ) { * @return string|false The output of the script. */ private function run_behat_tags_script( $env = '' ) { - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + $behat_tags = dirname( dirname( __DIR__ ) ) . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'behat-tags.php'; // Use the `-n` flag to disable loading of `php.ini` and ensure a clean environment. $php_run = escapeshellarg( PHP_BINARY ) . ' -n ' . escapeshellarg( $behat_tags ); @@ -267,7 +267,7 @@ public function test_behat_tags_extension(): void { public function test_behat_tags_db_version(): void { $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); - $behat_tags = dirname( dirname( __DIR__ ) ) . '/utils/behat-tags.php'; + $behat_tags = dirname( dirname( __DIR__ ) ) . DIRECTORY_SEPARATOR . 'utils' . DIRECTORY_SEPARATOR . 'behat-tags.php'; // Just to get the get_db_version() function. Prevents unexpected output. ob_start(); From 0056f0a99f9e500110daf11e9c9ddd394091365f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:09:27 +0200 Subject: [PATCH 074/107] another one --- tests/tests/TestBehatTags.php | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index a84b465ac..59f8b411b 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -68,18 +68,23 @@ private function run_behat_tags_script( $env = '' ) { // Use the `-n` flag to disable loading of `php.ini` and ensure a clean environment. $php_run = escapeshellarg( PHP_BINARY ) . ' -n ' . escapeshellarg( $behat_tags ); + $features_dir = $this->temp_dir . DIRECTORY_SEPARATOR . 'features'; + $command = ''; - if ( ! empty( $env ) ) { - // putenv() can be unreliable. Prepending the variable to the command is more robust. - if ( Utils\is_windows() ) { - // `set` is internal to `cmd.exe`. Do not escape the $env variable, as it's from a trusted - // data provider and `escapeshellarg` adds quotes that `set` doesn't understand. - // Note: `set "VAR=VALUE"` is more robust than `set VAR=VALUE`. - $command = 'set "' . $env . '" && '; - } else { - // On Unix-like systems, this sets the variable for the duration of the command. - $command = $env . ' '; + if ( Utils\is_windows() ) { + // `set` is internal to `cmd.exe`. Do not escape the values, as `set` doesn't understand quotes from escapeshellarg. + // Note: `set "VAR=VALUE"` is more robust than `set VAR=VALUE`. + $command = 'set "BEHAT_FEATURES_FOLDER=' . $features_dir . '" && '; + if ( ! empty( $env ) ) { + $command .= 'set "' . $env . '" && '; } + } else { + // On Unix-like systems, this sets the variable for the duration of the command. + $command = 'BEHAT_FEATURES_FOLDER=' . escapeshellarg( $features_dir ); + if ( ! empty( $env ) ) { + $command .= ' ' . $env; + } + $command .= ' '; } $command = 'cd ' . escapeshellarg( $this->temp_dir ) . ' && ' . $command . $php_run; @@ -87,7 +92,6 @@ private function run_behat_tags_script( $env = '' ) { return $output; } - /** * @dataProvider data_behat_tags_wp_version_github_token * From 8534057ce4c186194c4ff6da663eea866016a842 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:22:14 +0200 Subject: [PATCH 075/107] Try IPv4 resolution on Windows for Composer --- src/Context/FeatureContext.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 8a69c31de..a82c56a81 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1200,12 +1200,12 @@ public function drop_db(): void { * @param string $path * @return Process */ - public function proc( $command, $assoc_args = [], $path = '' ): Process { + public function proc( $command, $assoc_args = [], $path = '', $extra_env = [] ): Process { if ( ! empty( $assoc_args ) ) { $command .= Utils\assoc_args_to_str( $assoc_args ); } - $env = self::get_process_env_variables(); + $env = array_merge( self::get_process_env_variables(), $extra_env ); if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; @@ -1604,7 +1604,11 @@ private function composer_command( $cmd ): void { $path = strtok( $path, PHP_EOL ); $this->variables['COMPOSER_PATH'] = $path; } - $this->proc( $this->variables['COMPOSER_PATH'] . ' --no-interaction ' . $cmd )->run_check(); + $extra_env = []; + if ( Utils\is_windows() ) { + $extra_env['COMPOSER_IPRESOLVE'] = '4'; + } + $this->proc( $this->variables['COMPOSER_PATH'] . ' --no-interaction ' . $cmd, [], '', $extra_env )->run_check(); } /** From 7156d9652dbd77055e06656fcd82a9be09f1b949 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:23:42 +0200 Subject: [PATCH 076/107] update docblock --- src/Context/FeatureContext.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index a82c56a81..3dab931bd 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1198,6 +1198,7 @@ public function drop_db(): void { * @param string $command * @param array $assoc_args * @param string $path + * @param array $extra_env * @return Process */ public function proc( $command, $assoc_args = [], $path = '', $extra_env = [] ): Process { From a5cc4d1f94eb0290b45ffe1b1e907db2e0830465 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:31:13 +0200 Subject: [PATCH 077/107] Revert "Try IPv4 resolution on Windows for Composer" This reverts commit 8534057ce4c186194c4ff6da663eea866016a842. --- src/Context/FeatureContext.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 3dab931bd..6ccdc5120 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1201,12 +1201,12 @@ public function drop_db(): void { * @param array $extra_env * @return Process */ - public function proc( $command, $assoc_args = [], $path = '', $extra_env = [] ): Process { + public function proc( $command, $assoc_args = [], $path = '' ): Process { if ( ! empty( $assoc_args ) ) { $command .= Utils\assoc_args_to_str( $assoc_args ); } - $env = array_merge( self::get_process_env_variables(), $extra_env ); + $env = self::get_process_env_variables(); if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; @@ -1605,11 +1605,7 @@ private function composer_command( $cmd ): void { $path = strtok( $path, PHP_EOL ); $this->variables['COMPOSER_PATH'] = $path; } - $extra_env = []; - if ( Utils\is_windows() ) { - $extra_env['COMPOSER_IPRESOLVE'] = '4'; - } - $this->proc( $this->variables['COMPOSER_PATH'] . ' --no-interaction ' . $cmd, [], '', $extra_env )->run_check(); + $this->proc( $this->variables['COMPOSER_PATH'] . ' --no-interaction ' . $cmd )->run_check(); } /** From 134e6261276a3a8aac0f7f353d21e9e64c51b538 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:31:13 +0200 Subject: [PATCH 078/107] Revert "update docblock" This reverts commit 7156d9652dbd77055e06656fcd82a9be09f1b949. --- src/Context/FeatureContext.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 6ccdc5120..8a69c31de 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1198,7 +1198,6 @@ public function drop_db(): void { * @param string $command * @param array $assoc_args * @param string $path - * @param array $extra_env * @return Process */ public function proc( $command, $assoc_args = [], $path = '' ): Process { From b44d869f5c02ae1cd75ba2548e0de90c341b277b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:33:32 +0200 Subject: [PATCH 079/107] cleanup --- src/Context/FeatureContext.php | 3 +- src/Context/Process.php | 181 --------------------------------- 2 files changed, 1 insertion(+), 183 deletions(-) delete mode 100644 src/Context/Process.php diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 8a69c31de..eccd833aa 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -13,7 +13,6 @@ use Behat\Behat\Hook\Scope\AfterFeatureScope; use Behat\Behat\Hook\Scope\BeforeFeatureScope; use Behat\Behat\Hook\Scope\BeforeStepScope; -use Behat\Testwork\Hook\Scope\HookScope; use SebastianBergmann\CodeCoverage\Report\Clover; use SebastianBergmann\CodeCoverage\Driver\Selector; use SebastianBergmann\CodeCoverage\Driver\Xdebug; @@ -21,8 +20,8 @@ use SebastianBergmann\CodeCoverage\CodeCoverage; use SebastianBergmann\Environment\Runtime; use RuntimeException; -use WP_CLI; use DirectoryIterator; +use WP_CLI\Process; use WP_CLI\ProcessRun; use WP_CLI\Utils; use WP_CLI\WpOrgApi; diff --git a/src/Context/Process.php b/src/Context/Process.php deleted file mode 100644 index 12d845513..000000000 --- a/src/Context/Process.php +++ /dev/null @@ -1,181 +0,0 @@ - STDIN, - 1 => [ 'pipe', 'w' ], - 2 => [ 'pipe', 'w' ], - ]; - - /** - * @var bool Whether to log run time info or not. - */ - public static $log_run_times = false; - - /** - * @var array Array of process run time info, keyed by process command, each a 2-element array containing run time and run count. - */ - public static $run_times = []; - - /** - * @param string $command Command to execute. - * @param string|null $cwd Directory to execute the command in. - * @param array|null $env Environment variables to set when running the command. - * - * @return Process - */ - public static function create( $command, $cwd = null, $env = [] ) { - $proc = new self(); - - $proc->command = $command; - $proc->cwd = $cwd; - $proc->env = $env; - - return $proc; - } - - private function __construct() {} - - /** - * Run the command. - * - * @return \WP_CLI\ProcessRun - */ - public function run() { - \WP_CLI\Utils\check_proc_available( 'Process::run' ); - - $start_time = microtime( true ); - - $pipes = []; - if ( \WP_CLI\Utils\is_windows() ) { - // On Windows, leaving pipes open can cause hangs. - // Redirect output to files and close stdin. - $stdout_file = tempnam( sys_get_temp_dir(), 'behat-stdout-' ); - $stderr_file = tempnam( sys_get_temp_dir(), 'behat-stderr-' ); - $descriptors = [ - 0 => [ 'pipe', 'r' ], - 1 => [ 'file', $stdout_file, 'a' ], - 2 => [ 'file', $stderr_file, 'a' ], - ]; - $proc = \WP_CLI\Utils\proc_open_compat( $this->command, $descriptors, $pipes, $this->cwd, $this->env ); - fclose( $pipes[0] ); - } else { - $proc = \WP_CLI\Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); - $stdout = stream_get_contents( $pipes[1] ); - fclose( $pipes[1] ); - $stderr = stream_get_contents( $pipes[2] ); - fclose( $pipes[2] ); - } - - $return_code = proc_close( $proc ); - - if ( \WP_CLI\Utils\is_windows() ) { - $stdout = file_get_contents( $stdout_file ); - $stderr = file_get_contents( $stderr_file ); - unlink( $stdout_file ); - unlink( $stderr_file ); - - // Normalize line endings. - $stdout = str_replace( "\r\n", "\n", $stdout ); - $stderr = str_replace( "\r\n", "\n", $stderr ); - } - - $run_time = microtime( true ) - $start_time; - - if ( self::$log_run_times ) { - if ( ! isset( self::$run_times[ $this->command ] ) ) { - self::$run_times[ $this->command ] = [ 0, 0 ]; - } - self::$run_times[ $this->command ][0] += $run_time; - ++self::$run_times[ $this->command ][1]; - } - - return new \WP_CLI\ProcessRun( - [ - 'stdout' => $stdout, - 'stderr' => $stderr, - 'return_code' => $return_code, - 'command' => $this->command, - 'cwd' => $this->cwd, - 'env' => $this->env, - 'run_time' => $run_time, - ] - ); - } - - /** - * Run the command, but throw an Exception on error. - * - * @return \WP_CLI\ProcessRun - */ - public function run_check() { - $r = $this->run(); - - if ( $r->return_code ) { - throw new RuntimeException( $r ); - } - - return $r; - } - - /** - * Run the command, but throw an Exception on error. - * Same as `run_check()` above, but checks the correct stderr. - * - * @return \WP_CLI\ProcessRun - */ - public function run_check_stderr() { - $r = $this->run(); - - if ( $r->return_code ) { - throw new RuntimeException( $r ); - } - - if ( ! empty( $r->stderr ) ) { - // If the only thing that STDERR caught was the Requests deprecated message, ignore it. - // This is a temporary fix until we have a better solution for dealing with Requests - // as a dependency shared between WP Core and WP-CLI. - $stderr_lines = array_filter( explode( "\n", $r->stderr ) ); - if ( 1 === count( $stderr_lines ) ) { - $stderr_line = $stderr_lines[0]; - if ( - false !== strpos( - $stderr_line, - 'The PSR-0 `Requests_...` class names in the Request library are deprecated.' - ) - ) { - return $r; - } - } - - throw new RuntimeException( $r ); - } - - return $r; - } -} From 0b78aa0776a524098b58f9b7bf458774b922ce88 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 12 Oct 2025 20:45:52 +0200 Subject: [PATCH 080/107] add return type --- tests/tests/TestBehatTags.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 59f8b411b..5b581bc14 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -35,7 +35,7 @@ protected function tear_down(): void { * * @param string $dir The directory to remove. */ - private function remove_dir( $dir ) { + private function remove_dir( $dir ): void { if ( ! is_dir( $dir ) ) { return; } From 630f94758e7a0bb2ed23ab08b410c7bf7e5ceda5 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Oct 2025 09:36:42 +0200 Subject: [PATCH 081/107] Use `main` branch again --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index dec9fe2f8..94241454e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -12,7 +12,7 @@ on: jobs: test: - uses: wp-cli/.github/.github/workflows/reusable-testing.yml@try/test-suite + uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main with: matrix: | { From 196eba3661122fe2622af92830a6daa5dbf39d5d Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 13 Oct 2025 09:45:45 +0200 Subject: [PATCH 082/107] Restore warning --- src/Context/FeatureContext.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index eccd833aa..9b81a9009 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -437,6 +437,13 @@ private static function get_process_env_variables(): array { wp_cli_behat_env_debug( "WP-CLI binary path: {$bin_path}" ); + $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; + $full_bin_path = $bin_path . DIRECTORY_SEPARATOR . $bin; + + if ( ! is_executable( $full_bin_path ) ) { + wp_cli_behat_env_debug( "WARNING: File named '{$bin}' found in the provided/detected binary path is not executable." ); + } + $path_separator = Utils\is_windows() ? ';' : ':'; $php_binary_path = dirname( PHP_BINARY ); $env = [ @@ -1232,8 +1239,6 @@ public function proc( $command, $assoc_args = [], $path = '' ): Process { $cwd = null; } - wp_cli_behat_env_debug( "Running command: {$command}" ); - return Process::create( $command, $cwd, $env ); } From 7b92d184502b48bdba48e71b7323a771538f0d7f Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Oct 2025 11:08:35 -0700 Subject: [PATCH 083/107] Temporarily use different branch --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 01e690e90..6d9cf9977 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "wp-cli/config-command": "^1 || ^2", "wp-cli/core-command": "^1 || ^2", "wp-cli/eval-command": "^1 || ^2", - "wp-cli/wp-cli": "^2.12", + "wp-cli/wp-cli": "dev-try/win", "wp-coding-standards/wpcs": "^3", "yoast/phpunit-polyfills": "^4.0.0" }, From 596dba3d01d2edccecfd7ee3d879a6af02ecc253 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Oct 2025 12:19:27 -0700 Subject: [PATCH 084/107] Revert "Temporarily use different branch" This reverts commit 7b92d184502b48bdba48e71b7323a771538f0d7f. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6d9cf9977..01e690e90 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "wp-cli/config-command": "^1 || ^2", "wp-cli/core-command": "^1 || ^2", "wp-cli/eval-command": "^1 || ^2", - "wp-cli/wp-cli": "dev-try/win", + "wp-cli/wp-cli": "^2.12", "wp-coding-standards/wpcs": "^3", "yoast/phpunit-polyfills": "^4.0.0" }, From bbc90462d03052eb3e16cfdd87ae9cc3f63b7adf Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Oct 2025 12:47:26 -0700 Subject: [PATCH 085/107] Use wp-cli main branch --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 01e690e90..e91479f32 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "wp-cli/config-command": "^1 || ^2", "wp-cli/core-command": "^1 || ^2", "wp-cli/eval-command": "^1 || ^2", - "wp-cli/wp-cli": "^2.12", + "wp-cli/wp-cli": "^2.13", "wp-coding-standards/wpcs": "^3", "yoast/phpunit-polyfills": "^4.0.0" }, From 630b4da32078d8c707c6ef47b65a31207a5151e7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 22 Oct 2025 12:52:09 -0700 Subject: [PATCH 086/107] Update PHPStan config --- phpstan.neon.dist | 2 -- 1 file changed, 2 deletions(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 3c2a0976c..f643eed45 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -19,8 +19,6 @@ parameters: - WP_DEBUG_LOG - WP_DEBUG_DISPLAY ignoreErrors: - # Needs fixing in WP-CLI. - - message: '#Parameter \#1 \$cmd of function WP_CLI\\Utils\\esc_cmd expects array#' - message: '#Dynamic call to static method#' path: 'tests/tests' strictRules: From e438c09a3e9939116d3646dd6511f802ca045e7a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 20:33:12 +0100 Subject: [PATCH 087/107] Update src/Context/FeatureContext.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Context/FeatureContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 9b81a9009..db454a9f2 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -454,7 +454,7 @@ private static function get_process_env_variables(): array { 'TEST_RUN_DIR' => self::$behat_run_dir, ]; - $env = array_merge( $_ENV, $env ); + $env = array_merge( $env, $_ENV ); if ( self::running_with_code_coverage() ) { $has_coverage_driver = ( new Runtime() )->hasXdebug() || ( new Runtime() )->hasPCOV(); From 2d38a51e093881d3b26268362578e2d742a86efa Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 20:34:09 +0100 Subject: [PATCH 088/107] Cast to string --- utils/behat-tags.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/behat-tags.php b/utils/behat-tags.php index c11c49d57..fcf4ec6b9 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -27,7 +27,7 @@ function version_tags( $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); if ( ! empty( $feature_files ) ) { foreach ( $feature_files as $feature_file ) { - $contents = file_get_contents( $feature_file ); + $contents = (string) file_get_contents( $feature_file ); if ( preg_match_all( '/@' . $prefix . '-[0-9\.]+/', $contents, $matches ) ) { $existing_tags = array_merge( $existing_tags, $matches[0] ); } @@ -130,7 +130,7 @@ function extension_tags( $features_folder = 'features' ) { $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); if ( ! empty( $feature_files ) ) { foreach ( $feature_files as $feature_file ) { - $contents = file_get_contents( $feature_file ); + $contents = (string) get_contents( $feature_file ); if ( preg_match_all( '/@require-extension-[A-Za-z_]*/', $contents, $matches ) ) { $extension_tags = array_merge( $extension_tags, $matches[0] ); } From 835e2b47b9195034aa606189d26b3ad93543f065 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 28 Oct 2025 15:23:24 +0100 Subject: [PATCH 089/107] Composer: adjust version ranges --- composer.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index e91479f32..e9ea54ab0 100644 --- a/composer.json +++ b/composer.json @@ -16,18 +16,18 @@ "php-parallel-lint/php-parallel-lint": "^1.3.1", "phpcompatibility/php-compatibility": "dev-develop", "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^1.12.26", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "swissspidy/phpstan-no-private": "^0.2.1", + "phpstan/phpstan": "^1.12.26 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.2 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "swissspidy/phpstan-no-private": "^0.2.1 || ^1.0", "szepeviktor/phpstan-wordpress": "^v1.3.5", "wp-cli/config-command": "^1 || ^2", "wp-cli/core-command": "^1 || ^2", "wp-cli/eval-command": "^1 || ^2", "wp-cli/wp-cli": "^2.13", "wp-coding-standards/wpcs": "^3", - "yoast/phpunit-polyfills": "^4.0.0" + "yoast/phpunit-polyfills": "^1.0 || ^2.0 || ^3.0 || ^4.0" }, "require-dev": { "roave/security-advisories": "dev-latest" From da745566814abb5b256b387aa7d07d49b293c59f Mon Sep 17 00:00:00 2001 From: swissspidy Date: Tue, 11 Nov 2025 13:31:07 +0000 Subject: [PATCH 090/107] Update file(s) from wp-cli/.github --- AGENTS.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..1ff84f6d1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,121 @@ +# Instructions + +This package is part of WP-CLI, the official command line interface for WordPress. For a detailed explanation of the project structure and development workflow, please refer to the main @README.md file. + +## Best Practices for Code Contributions + +When contributing to this package, please adhere to the following guidelines: + +* **Follow Existing Conventions:** Before writing any code, analyze the existing codebase in this package to understand the coding style, naming conventions, and architectural patterns. +* **Focus on the Package's Scope:** All changes should be relevant to the functionality of the package. +* **Write Tests:** All new features and bug fixes must be accompanied by acceptance tests using Behat. You can find the existing tests in the `features/` directory. There may be PHPUnit unit tests as well in the `tests/` directory. +* **Update Documentation:** If your changes affect the user-facing functionality, please update the relevant inline code documentation. + +### Building and running + +Before submitting any changes, it is crucial to validate them by running the full suite of static code analysis and tests. To run the full suite of checks, execute the following command: `composer test`. + +This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps separately, it is highly recommended to use this single command to ensure a comprehensive validation. + +### Useful Composer Commands + +The project uses Composer to manage dependencies and run scripts. The following commands are available: + +* `composer install`: Install dependencies. +* `composer test`: Run the full test suite, including linting, code style checks, static analysis, and unit/behavior tests. +* `composer lint`: Check for syntax errors. +* `composer phpcs`: Check for code style violations. +* `composer phpcbf`: Automatically fix code style violations. +* `composer phpstan`: Run static analysis. +* `composer phpunit`: Run unit tests. +* `composer behat`: Run behavior-driven tests. + +### Coding Style + +The project follows the `WP_CLI_CS` coding standard, which is enforced by PHP_CodeSniffer. The configuration can be found in `phpcs.xml.dist`. Before submitting any code, please run `composer phpcs` to check for violations and `composer phpcbf` to automatically fix them. + +## Documentation + +The `README.md` file might be generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). In that case, changes need to be made against the corresponding part of the codebase. + +### Inline Documentation + +Only write high-value comments if at all. Avoid talking to the user through comments. + +## Testing + +The project has a comprehensive test suite that includes unit tests, behavior-driven tests, and static analysis. + +* **Unit tests** are written with PHPUnit and can be found in the `tests/` directory. The configuration is in `phpunit.xml.dist`. +* **Behavior-driven tests** are written with Behat and can be found in the `features/` directory. The configuration is in `behat.yml`. +* **Static analysis** is performed with PHPStan. + +All tests are run on GitHub Actions for every pull request. + +When writing tests, aim to follow existing patterns. Key conventions include: + +* When adding tests, first examine existing tests to understand and conform to established conventions. +* For unit tests, extend the base `WP_CLI\Tests\TestCase` test class. +* For Behat tests, only WP-CLI commands installed in `composer.json` can be run. + +### Behat Steps + +WP-CLI makes use of a Behat-based testing framework and provides a set of custom step definitions to write feature tests. + +> **Note:** If you are expecting an error output in a test, you need to use `When I try ...` instead of `When I run ...` . + +#### Given + +* `Given an empty directory` - Creates an empty directory. +* `Given /^an? (empty|non-existent) ([^\s]+) directory$/` - Creates or deletes a specific directory. +* `Given an empty cache` - Clears the WP-CLI cache directory. +* `Given /^an? ([^\s]+) (file|cache file):$/` - Creates a file with the given contents. +* `Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/` - Search and replace a string in a file using regex. +* `Given /^that HTTP requests to (.*?) will respond with:$/` - Mock HTTP requests to a given URL. +* `Given WP files` - Download WordPress files without installing. +* `Given wp-config.php` - Create a wp-config.php file using `wp config create`. +* `Given a database` - Creates an empty database. +* `Given a WP install(ation)` - Installs WordPress. +* `Given a WP install(ation) in :subdir` - Installs WordPress in a given directory. +* `Given a WP install(ation) with Composer` - Installs WordPress with Composer. +* `Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory` - Installs WordPress with Composer and a custom vendor directory. +* `Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/` - Installs WordPress Multisite. +* `Given these installed and active plugins:` - Installs and activates one or more plugins. +* `Given a custom wp-content directory` - Configure a custom `wp-content` directory. +* `Given download:` - Download multiple files into the given destinations. +* `Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/` - Store STDOUT or STDERR contents in a variable. +* `Given /^a new Phar with (?:the same version|version "([^"]+)")$/` - Build a new WP-CLI Phar file with a given version. +* `Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/` - Download a specific WP-CLI Phar version from GitHub. +* `Given /^save the (.+) file ([\'].+[^\'])? as \{(\w+)\}$/` - Stores the contents of the given file in a variable. +* `Given a misconfigured WP_CONTENT_DIR constant directory` - Modify wp-config.php to set `WP_CONTENT_DIR` to an empty string. +* `Given a dependency on current wp-cli` - Add `wp-cli/wp-cli` as a Composer dependency. +* `Given a PHP built-in web server` - Start a PHP built-in web server in the current directory. +* `Given a PHP built-in web server to serve :subdir` - Start a PHP built-in web server in the given subdirectory. + +#### When + +* ``When /^I launch in the background `([^`]+)`$/`` - Launch a given command in the background. +* ``When /^I (run|try) `([^`]+)`$/`` - Run or try a given command. +* ``When /^I (run|try) `([^`]+)` from '([^\s]+)'$/`` - Run or try a given command in a subdirectory. +* `When /^I (run|try) the previous command again$/` - Run or try the previous command again. + +#### Then + +* `Then /^the return code should( not)? be (\d+)$/` - Expect a specific exit code of the previous command. +* `Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/` - Check the contents of STDOUT or STDERR. +* `Then /^(STDOUT|STDERR) should be a number$/` - Expect STDOUT or STDERR to be a numeric value. +* `Then /^(STDOUT|STDERR) should not be a number$/` - Expect STDOUT or STDERR to not be a numeric value. +* `Then /^STDOUT should be a table containing rows:$/` - Expect STDOUT to be a table containing the given rows. +* `Then /^STDOUT should end with a table containing rows:$/` - Expect STDOUT to end with a table containing the given rows. +* `Then /^STDOUT should be JSON containing:$/` - Expect valid JSON output in STDOUT. +* `Then /^STDOUT should be a JSON array containing:$/` - Expect valid JSON array output in STDOUT. +* `Then /^STDOUT should be CSV containing:$/` - Expect STDOUT to be CSV containing certain values. +* `Then /^STDOUT should be YAML containing:$/` - Expect STDOUT to be YAML containing certain content. +* `Then /^(STDOUT|STDERR) should be empty$/` - Expect STDOUT or STDERR to be empty. +* `Then /^(STDOUT|STDERR) should not be empty$/` - Expect STDOUT or STDERR not to be empty. +* `Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|<>) ([+\w.{}-]+)$/` - Expect STDOUT or STDERR to be a version string comparing to the given version. +* `Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain):$/` - Expect a certain file or directory to (not) exist or (not) contain certain contents. +* `Then /^the contents of the (.+) file should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match file contents against a regex. +* `Then /^(STDOUT|STDERR) should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match STDOUT or STDERR against a regex. +* `Then /^an email should (be sent|not be sent)$/` - Expect an email to be sent (or not). +* `Then the HTTP status code should be :code` - Expect the HTTP status code for visiting `http://localhost:8080`. From 508b043c9d2e40107a6fc2dabd1a7e0d5ff02ea4 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 20:41:42 +0100 Subject: [PATCH 091/107] Apply some suggestions --- src/Context/FeatureContext.php | 14 ++++++++++---- tests/tests/TestBehatTags.php | 4 ++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index db454a9f2..333ef8238 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -606,6 +606,12 @@ private static function configure_sqlite( $dir ): void { $db_copy = $dir . '/wp-content/mu-plugins/sqlite-database-integration/db.copy'; $db_dropin = $dir . '/wp-content/db.php'; + $db_copy_contents = file_get_contents( $db_copy ); + + if ( false === $db_copy_contents ) { + return; + } + /* similar to https://github.com/WordPress/sqlite-database-integration/blob/3306576c9b606bc23bbb26c15383fef08e03ab11/activate.php#L95 */ $file_contents = str_replace( array( @@ -618,7 +624,7 @@ private static function configure_sqlite( $dir ): void { 'sqlite-database-integration/load.php', '/mu-plugins/', ), - file_get_contents( $db_copy ) + $db_copy_contents ); file_put_contents( $db_dropin, $file_contents ); @@ -916,7 +922,7 @@ private function replace_invoke_wp_cli_with_php_args( $str ) { $phar_begin_len = strlen( $phar_begin ); $bin_dir = getenv( 'WP_CLI_BIN_DIR' ); $bin = Utils\is_windows() ? 'wp.bat' : 'wp'; - if ( false !== $bin_dir && file_exists( $bin_dir . '/wp' ) && file_get_contents( $bin_dir . '/wp', false, null, 0, $phar_begin_len ) === $phar_begin ) { + if ( false !== $bin_dir && file_exists( $bin_dir . DIRECTORY_SEPARATOR . $bin ) && (string) file_get_contents( $bin_dir . DIRECTORY_SEPARATOR . $bin, false, null, 0, $phar_begin_len ) === $phar_begin ) { $phar_path = $bin_dir . $bin; } else { $src_dir = dirname( __DIR__, 2 ); @@ -1278,7 +1284,7 @@ public function background_proc( $cmd ): void { if ( ! $status['running'] ) { if ( Utils\is_windows() ) { - $stderr = file_get_contents( $stderr_file ); + $stderr = (string) file_get_contents( $stderr_file ); $stderr = $stderr ? ': ' . $stderr : ''; } else { $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; @@ -1879,7 +1885,7 @@ function wpcli_bootstrap_behat_feature_context(): void { return; } - $composer = json_decode( file_get_contents( $project_composer ) ); + $composer = json_decode( (string) file_get_contents( $project_composer ) ); if ( empty( $composer->autoload->files ) ) { return; } diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 5b581bc14..a5558c34a 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -47,9 +47,9 @@ private function remove_dir( $dir ): void { foreach ( $iterator as $file ) { if ( $file->isDir() ) { - rmdir( $file->getRealPath() ); + rmdir( $file->getPathname() ); } else { - unlink( $file->getRealPath() ); + unlink( $file->getPathname() ); } } From 3682328b1a15fc74399e266253ad22bc773efbdd Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Mon, 24 Nov 2025 21:00:36 +0100 Subject: [PATCH 092/107] Fix --- utils/behat-tags.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/behat-tags.php b/utils/behat-tags.php index fcf4ec6b9..3f9188ea1 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -130,7 +130,7 @@ function extension_tags( $features_folder = 'features' ) { $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); if ( ! empty( $feature_files ) ) { foreach ( $feature_files as $feature_file ) { - $contents = (string) get_contents( $feature_file ); + $contents = (string) file_get_contents( $feature_file ); if ( preg_match_all( '/@require-extension-[A-Za-z_]*/', $contents, $matches ) ) { $extension_tags = array_merge( $extension_tags, $matches[0] ); } From 9ff648c55755e4c0dd255f140961dda96d82ccb9 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 25 Nov 2025 14:32:41 +0100 Subject: [PATCH 093/107] Add behat tags --- tests/tests/TestBehatTags.php | 58 ++++++++++++++++++++++++++++++ utils/behat-tags.php | 68 +++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index a5558c34a..3e18d6200 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -314,4 +314,62 @@ public function test_behat_tags_db_version(): void { $output = $this->run_behat_tags_script(); $this->assertSame( $expected, $output ); } + + public function test_behat_tags_os(): void { + $env_github_token = getenv( 'GITHUB_TOKEN' ); + $db_type = getenv( 'WP_CLI_TEST_DBTYPE' ); + + putenv( 'GITHUB_TOKEN' ); + + file_put_contents( $this->temp_dir . DIRECTORY_SEPARATOR . 'features' . DIRECTORY_SEPARATOR . 'os.feature', '@require-windows @skip-windows @require-macos @skip-macos @require-linux @skip-linux' ); + + $expecteds = array(); + + switch ( $db_type ) { + case 'mariadb': + $expecteds[] = '~@require-mysql'; + $expecteds[] = '~@require-sqlite'; + break; + case 'sqlite': + $expecteds[] = '~@require-mariadb'; + $expecteds[] = '~@require-mysql'; + $expecteds[] = '~@require-mysql-or-mariadb'; + break; + case 'mysql': + default: + $expecteds[] = '~@require-mariadb'; + $expecteds[] = '~@require-sqlite'; + break; + } + + // `PHP_OS_FAMILY` is available since PHP 7.2. + $is_windows = 'Windows' === PHP_OS_FAMILY; + $is_macos = 'Darwin' === PHP_OS_FAMILY; + $is_linux = 'Linux' === PHP_OS_FAMILY; + + if ( ! $is_windows ) { + $expecteds[] = '~@require-windows'; + } + if ( $is_windows ) { + $expecteds[] = '~@skip-windows'; + } + if ( ! $is_macos ) { + $expecteds[] = '~@require-macos'; + } + if ( $is_macos ) { + $expecteds[] = '~@skip-macos'; + } + if ( ! $is_linux ) { + $expecteds[] = '~@require-linux'; + } + if ( $is_linux ) { + $expecteds[] = '~@skip-linux'; + } + + $expected = '--tags=' . implode( '&&', array_merge( array( '~@github-api', '~@broken' ), $expecteds ) ); + $output = $this->run_behat_tags_script(); + $this->assertSame( $expected, $output ); + + putenv( false === $env_github_token ? 'GITHUB_TOKEN' : "GITHUB_TOKEN=$env_github_token" ); + } } diff --git a/utils/behat-tags.php b/utils/behat-tags.php index 3f9188ea1..e84b6e68b 100644 --- a/utils/behat-tags.php +++ b/utils/behat-tags.php @@ -151,7 +151,75 @@ function extension_tags( $features_folder = 'features' ) { return $skip_tags; } +/** + * An array of tags for excluding tests based on the operating system. + * + * @param string $features_folder The folder where the feature files are located. + * @return array + */ +function os_tags( $features_folder = 'features' ) { + $os_tags = array(); + $feature_files = glob( $features_folder . DIRECTORY_SEPARATOR . '*.feature' ); + if ( ! empty( $feature_files ) ) { + foreach ( $feature_files as $feature_file ) { + $contents = (string) file_get_contents( $feature_file ); + if ( preg_match_all( '/@(require-(windows|macos|linux)|skip-(windows|macos|linux))/', $contents, $matches ) ) { + $os_tags = array_merge( $os_tags, $matches[0] ); + } + } + $os_tags = array_unique( $os_tags ); + } + + if ( empty( $os_tags ) ) { + return array(); + } + + $skip_tags = array(); + + $is_windows = 'Windows' === PHP_OS_FAMILY; + $is_macos = 'Darwin' === PHP_OS_FAMILY; + $is_linux = 'Linux' === PHP_OS_FAMILY; + + foreach ( $os_tags as $tag ) { + switch ( $tag ) { + case '@require-windows': + if ( ! $is_windows ) { + $skip_tags[] = $tag; + } + break; + case '@require-macos': + if ( ! $is_macos ) { + $skip_tags[] = $tag; + } + break; + case '@require-linux': + if ( ! $is_linux ) { + $skip_tags[] = $tag; + } + break; + case '@skip-windows': + if ( $is_windows ) { + $skip_tags[] = $tag; + } + break; + case '@skip-macos': + if ( $is_macos ) { + $skip_tags[] = $tag; + } + break; + case '@skip-linux': + if ( $is_linux ) { + $skip_tags[] = $tag; + } + break; + } + } + + return $skip_tags; +} + $skip_tags = array_merge( $skip_tags, extension_tags( $features_folder ) ); +$skip_tags = array_merge( $skip_tags, os_tags( $features_folder ) ); if ( ! empty( $skip_tags ) ) { echo '--tags=~' . implode( '&&~', $skip_tags ); From c501401595e8578cc2671acc4e1afa3a3ab099dc Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 25 Nov 2025 14:33:22 +0100 Subject: [PATCH 094/107] Remove comment --- tests/tests/TestBehatTags.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/tests/TestBehatTags.php b/tests/tests/TestBehatTags.php index 3e18d6200..13b222860 100644 --- a/tests/tests/TestBehatTags.php +++ b/tests/tests/TestBehatTags.php @@ -342,7 +342,6 @@ public function test_behat_tags_os(): void { break; } - // `PHP_OS_FAMILY` is available since PHP 7.2. $is_windows = 'Windows' === PHP_OS_FAMILY; $is_macos = 'Darwin' === PHP_OS_FAMILY; $is_linux = 'Linux' === PHP_OS_FAMILY; From 7d23c9eba6eb09edd3eb4eab9073a77182835b04 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 15:48:03 +0100 Subject: [PATCH 095/107] recursive mkdir --- src/Context/FeatureContext.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 6f8e9fb44..4336975c6 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1387,13 +1387,13 @@ public function download_wp( $subdir = '', $version = '' ): void { $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; if ( $subdir ) { - mkdir( $dest_dir ); + mkdir( $dest_dir, 0777, true /*recursive*/ ); } self::copy_dir( self::$cache_dir, $dest_dir ); if ( ! is_dir( $dest_dir . '/wp-content/mu-plugins' ) ) { - mkdir( $dest_dir . '/wp-content/mu-plugins' ); + mkdir( $dest_dir . '/wp-content/mu-plugins', 0777, true /*recursive*/ ); } // Disable emailing. From b84aea2488d16543c2382344a4568c0997e80b23 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 15:53:55 +0100 Subject: [PATCH 096/107] Avoid passing null to `explode` --- src/Context/FeatureContext.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 4336975c6..487189885 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -786,13 +786,15 @@ private static function terminate_proc( $master_pid ): void { $output = shell_exec( "ps -o ppid,pid,command | grep $master_pid" ); - foreach ( explode( PHP_EOL, $output ) as $line ) { - if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { - $parent = $matches[1]; - $child = $matches[2]; - - if ( (int) $parent === (int) $master_pid ) { - self::terminate_proc( (int) $child ); + if ( is_string( $output ) ) { + foreach ( explode( PHP_EOL, $output ) as $line ) { + if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { + $parent = $matches[1]; + $child = $matches[2]; + + if ( (int) $parent === (int) $master_pid ) { + self::terminate_proc( (int) $child ); + } } } } From f7d04a4492986f0180724fff9f29033297022274 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 16:04:11 +0100 Subject: [PATCH 097/107] Run test only on Linux --- features/behat-steps.feature | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index 817fa876a..4c406a166 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -426,8 +426,7 @@ Feature: Test that WP-CLI Behat steps work as expected wp-cli/wp-cli """ - - + @require-linux Scenario: Test STDOUT should be empty When I run `echo -n ""` Then STDOUT should be empty From ac2dfbc699f632866a1b6bdcb25943397fcc97a0 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 16:04:17 +0100 Subject: [PATCH 098/107] Skip test for Windows --- features/testing.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/testing.feature b/features/testing.feature index 48f379b41..3392d9ac6 100644 --- a/features/testing.feature +++ b/features/testing.feature @@ -57,7 +57,8 @@ Feature: Test that WP-CLI loads. sqlite """ - @require-sqlite + # Skipped on Windows because of curl getaddrinfo() errors. + @require-sqlite @skip-windows Scenario: Composer installation Given a WP install with Composer From 5e372663c4fd5d776c3289137aec32d98e062a64 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 16:04:21 +0100 Subject: [PATCH 099/107] Use PHP_EOL --- src/Context/ThenStepDefinitions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index bc2cb3f5b..d7cee6191 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -306,11 +306,11 @@ public function then_stdout_should_be_csv_containing( TableNode $expected ): voi // Convert expected rows to CSV format for diff. $expected_csv = ''; foreach ( $expected_rows as $row ) { - $expected_csv .= implode( ',', array_map( 'trim', $row ) ) . "\n"; + $expected_csv .= implode( ',', array_map( 'trim', $row ) ) . PHP_EOL; } $diff = $this->generate_diff( trim( $expected_csv ), trim( $output ) ); if ( ! empty( $diff ) ) { - $message .= "\n\n" . $diff; + $message .= PHP_EOL . PHP_EOL . $diff; } throw new Exception( $message ); } From f138f6b30995c88756fc90f7b5350dcfbe1efe17 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 16:07:03 +0100 Subject: [PATCH 100/107] More skips on windows --- features/behat-steps.feature | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index 4c406a166..5d0795565 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -403,7 +403,8 @@ Feature: Test that WP-CLI Behat steps work as expected | {SUITE_CACHE_DIR}/test.txt | https://www.iana.org/robots.txt | Then the {SUITE_CACHE_DIR}/test.txt file should exist - @require-wp @require-composer + # Skipped on Windows because of curl getaddrinfo() errors. + @require-wp @require-composer @skip-windows Scenario: Test WP installation with Composer Given a WP installation with Composer Then the composer.json file should exist @@ -411,13 +412,15 @@ Feature: Test that WP-CLI Behat steps work as expected When I run `wp core version` Then STDOUT should not be empty - @require-wp @require-composer + # Skipped on Windows because of curl getaddrinfo() errors. + @require-wp @require-composer @skip-windows Scenario: Test WP installation with Composer and custom vendor directory Given a WP installation with Composer and a custom vendor directory 'custom-vendor' Then the composer.json file should exist And the custom-vendor directory should exist - @require-wp @require-composer + # Skipped on Windows because of curl getaddrinfo() errors. + @require-wp @require-composer @skip-windows Scenario: Test dependency on current wp-cli Given a WP installation with Composer And a dependency on current wp-cli From e28fa885a009974854e82510f37e5f0232319391 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 16:24:21 +0100 Subject: [PATCH 101/107] Try using `DIRECTORY_SEPARATOR` --- src/Context/FeatureContext.php | 2 +- src/Context/ThenStepDefinitions.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Context/FeatureContext.php b/src/Context/FeatureContext.php index 487189885..d63f1e6d3 100644 --- a/src/Context/FeatureContext.php +++ b/src/Context/FeatureContext.php @@ -1042,7 +1042,7 @@ private static function get_event_file( $scope, &$line ): ?string { */ public function create_run_dir(): void { if ( ! isset( $this->variables['RUN_DIR'] ) ) { - self::$run_dir = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); + self::$run_dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', true ); $this->variables['RUN_DIR'] = self::$run_dir; mkdir( $this->variables['RUN_DIR'] ); } diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index d7cee6191..0ef93ec7a 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -456,8 +456,8 @@ public function then_a_specific_file_folder_should_exist( $path, $type, $strictl $path = $this->replace_variables( $path ); // If it's a relative path, make it relative to the current test dir. - if ( '/' !== $path[0] ) { - $path = $this->variables['RUN_DIR'] . "/$path"; + if ( DIRECTORY_SEPARATOR !== $path[0] ) { + $path = $this->variables['RUN_DIR'] . DIRECTORY_SEPARATOR . $path; } $exists = static function ( $path ) use ( $type ) { From 13be2f31fc5f0c84d3112af12c03477cc4259b84 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Tue, 9 Dec 2025 16:24:26 +0100 Subject: [PATCH 102/107] Ensure RUN_DIR is defined --- features/behat-steps.feature | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index 5d0795565..c60b3de8a 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -28,7 +28,8 @@ Feature: Test that WP-CLI Behat steps work as expected Then the test-dir directory should not exist Scenario: Test "Given an empty cache" step - Given an empty cache + Given a WP installation + And an empty cache Then the {SUITE_CACHE_DIR} directory should exist Scenario: Test "Given a file" step @@ -44,7 +45,8 @@ Feature: Test that WP-CLI Behat steps work as expected """ Scenario: Test "Given a cache file" step - Given an empty cache + Given a WP installation + And an empty cache And a test-cache.txt cache file: """ Cached content From e5b728088baa608b428f4f203b93da79e82c822e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Dec 2025 09:54:28 +0100 Subject: [PATCH 103/107] Fix relative path check --- src/Context/ThenStepDefinitions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index 0ef93ec7a..4dfc2c343 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -456,7 +456,7 @@ public function then_a_specific_file_folder_should_exist( $path, $type, $strictl $path = $this->replace_variables( $path ); // If it's a relative path, make it relative to the current test dir. - if ( DIRECTORY_SEPARATOR !== $path[0] ) { + if ( DIRECTORY_SEPARATOR !== $path[0] && 0 !== strpos( $path, $this->variables['RUN_DIR'] ) ) { $path = $this->variables['RUN_DIR'] . DIRECTORY_SEPARATOR . $path; } From 782e311586eb992d5644d71e71672eda88d47508 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Dec 2025 09:54:41 +0100 Subject: [PATCH 104/107] Ensure run_dir is set --- features/behat-steps.feature | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index c60b3de8a..896764fb5 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -399,7 +399,8 @@ Feature: Test that WP-CLI Behat steps work as expected @require-download Scenario: Test download step - Given an empty cache + Given a WP installation + And an empty cache And download: | path | url | | {SUITE_CACHE_DIR}/test.txt | https://www.iana.org/robots.txt | From d6e2506e64eb691a31f98932719d9aaaf1faf764 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Dec 2025 09:54:44 +0100 Subject: [PATCH 105/107] Revert "Use PHP_EOL" This reverts commit 5e372663c4fd5d776c3289137aec32d98e062a64. --- src/Context/ThenStepDefinitions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index 4dfc2c343..9bba4fd95 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -306,11 +306,11 @@ public function then_stdout_should_be_csv_containing( TableNode $expected ): voi // Convert expected rows to CSV format for diff. $expected_csv = ''; foreach ( $expected_rows as $row ) { - $expected_csv .= implode( ',', array_map( 'trim', $row ) ) . PHP_EOL; + $expected_csv .= implode( ',', array_map( 'trim', $row ) ) . "\n"; } $diff = $this->generate_diff( trim( $expected_csv ), trim( $output ) ); if ( ! empty( $diff ) ) { - $message .= PHP_EOL . PHP_EOL . $diff; + $message .= "\n\n" . $diff; } throw new Exception( $message ); } From 09a331fd5114a4ef0727f4603348b9006ae8d0c7 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Dec 2025 09:57:23 +0100 Subject: [PATCH 106/107] Tweak test --- features/behat-steps.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/behat-steps.feature b/features/behat-steps.feature index 896764fb5..8e12f9b94 100644 --- a/features/behat-steps.feature +++ b/features/behat-steps.feature @@ -681,3 +681,4 @@ Feature: Test that WP-CLI Behat steps work as expected Then STDOUT should be CSV containing: | user_login | user_email | | admin | admin@example.com | + | user2 | user2@example.com | From ef13e97980ab7a7f3145bedaeb82b18299c3485a Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 10 Dec 2025 11:55:34 +0100 Subject: [PATCH 107/107] fix abs path check --- src/Context/ThenStepDefinitions.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Context/ThenStepDefinitions.php b/src/Context/ThenStepDefinitions.php index 9bba4fd95..2b54f6eaf 100644 --- a/src/Context/ThenStepDefinitions.php +++ b/src/Context/ThenStepDefinitions.php @@ -455,8 +455,10 @@ public function then_stdout_stderr_should_be_a_specific_version_string( $stream, public function then_a_specific_file_folder_should_exist( $path, $type, $strictly, $action, $expected = null ): void { $path = $this->replace_variables( $path ); + $is_absolute = preg_match( '#^[a-zA-Z]:\\\\#', $path ) || ( '/' === $path[0] || '\\' === $path[0] ); + // If it's a relative path, make it relative to the current test dir. - if ( DIRECTORY_SEPARATOR !== $path[0] && 0 !== strpos( $path, $this->variables['RUN_DIR'] ) ) { + if ( ! $is_absolute ) { $path = $this->variables['RUN_DIR'] . DIRECTORY_SEPARATOR . $path; }