From b67ad2b1038e43fba7e3e5a6b7e5b16ef19a9f5b Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Thu, 2 Oct 2025 22:13:09 +0200 Subject: [PATCH 01/86] 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 02/86] 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 03/86] 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 04/86] 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 05/86] 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 06/86] 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 07/86] 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 08/86] 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 09/86] 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 10/86] 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 11/86] 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 12/86] 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 13/86] 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 14/86] 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 15/86] 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 16/86] 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 17/86] 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 18/86] 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 19/86] 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 20/86] 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 21/86] 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 22/86] 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 23/86] 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 24/86] 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 25/86] 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 26/86] 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 27/86] 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 28/86] `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 29/86] 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 30/86] 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 31/86] 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 32/86] 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 33/86] 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 34/86] 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 35/86] 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 36/86] 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 37/86] 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 38/86] 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 39/86] 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 40/86] 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 41/86] 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 42/86] 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 43/86] 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 44/86] 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 45/86] 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 46/86] 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 47/86] 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 48/86] 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 49/86] 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 50/86] 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 51/86] 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 52/86] 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 53/86] 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 54/86] 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 55/86] 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 56/86] 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 57/86] 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 58/86] 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 59/86] 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 60/86] 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 61/86] 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 62/86] 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 63/86] 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 64/86] 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 65/86] 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 66/86] 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 67/86] 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 68/86] 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 69/86] 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 70/86] 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 71/86] 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 72/86] 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 73/86] 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 74/86] 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 75/86] 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 76/86] 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 77/86] 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 78/86] 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 79/86] 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 80/86] 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 81/86] 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 82/86] 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 83/86] 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 84/86] 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 85/86] 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 86/86] 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: