diff --git a/.actrc b/.actrc deleted file mode 100644 index 99e6b7ecc..000000000 --- a/.actrc +++ /dev/null @@ -1,3 +0,0 @@ -# Configuration file for nektos/act. -# See https://github.com/nektos/act#configuration --P ubuntu-latest=shivammathur/node:latest diff --git a/.distignore b/.distignore index 95b52fb02..b964b40c7 100644 --- a/.distignore +++ b/.distignore @@ -6,8 +6,6 @@ .travis.yml behat.yml circle.yml -phpcs.xml.dist -phpunit.xml.dist bin/ features/ utils/ diff --git a/.editorconfig b/.editorconfig index 84f918ed5..fa483b1bb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,8 +4,6 @@ # WordPress Coding Standards # https://make.wordpress.org/core/handbook/coding-standards/ -# From https://github.com/WordPress/wordpress-develop/blob/trunk/.editorconfig with a couple of additions. - root = true [*] @@ -15,12 +13,13 @@ insert_final_newline = true trim_trailing_whitespace = true indent_style = tab -[{*.yml,*.feature,.jshintrc,*.json}] +[{.jshintrc,*.json,*.yml,*.feature}] indent_style = space indent_size = 2 -[*.md] -trim_trailing_whitespace = false - [{*.txt,wp-config-sample.php}] end_of_line = crlf + +[composer.json] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index d84f4ade2..000000000 --- a/.gitattributes +++ /dev/null @@ -1,14 +0,0 @@ -/.actrc export-ignore -/.distignore export-ignore -/.editorconfig export-ignore -/.github export-ignore -/.gitignore export-ignore -/.typos.toml export-ignore -/AGENTS.md export-ignore -/behat.yml export-ignore -/features export-ignore -/phpcs.xml.dist export-ignore -/phpstan.neon.dist export-ignore -/phpunit.xml.dist export-ignore -/tests export-ignore -/wp-cli.yml export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index f69375fb2..000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @wp-cli/committers diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index d6c7b8b04..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 2 -updates: - - package-ecosystem: composer - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 - labels: - - scope:distribution - - package-ecosystem: github-actions - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 - labels: - - scope:distribution - diff --git a/.github/settings.yml b/.github/settings.yml new file mode 100644 index 000000000..ef89f7872 --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,52 @@ +# Used by Probot Settings: https://probot.github.io/apps/settings/ +repository: + description: Manages object and transient caches. +labels: + - name: bug + color: fc2929 + - name: scope:documentation + color: 0e8a16 + - name: scope:testing + color: 5319e7 + - name: good-first-issue + color: eb6420 + - name: help-wanted + color: 159818 + - name: maybelater + color: c2e0c6 + - name: state:unconfirmed + color: bfe5bf + - name: state:unsupported + color: bfe5bf + - name: wontfix + color: c2e0c6 + - name: command:cache + color: c5def5 + - name: command:cache-add + color: c5def5 + - name: command:cache-decr + color: c5def5 + - name: command:cache-delete + color: c5def5 + - name: command:cache-flush + color: c5def5 + - name: command:cache-get + color: c5def5 + - name: command:cache-incr + color: c5def5 + - name: command:cache-replace + color: c5def5 + - name: command:cache-set + color: c5def5 + - name: command:cache-type + color: c5def5 + - name: command:transient + color: c5def5 + - name: command:transient-delete + color: c5def5 + - name: command:transient-get + color: c5def5 + - name: command:transient-set + color: c5def5 + - name: command:transient-type + color: c5def5 diff --git a/.github/workflows/check-branch-alias.yml b/.github/workflows/check-branch-alias.yml deleted file mode 100644 index 78da63710..000000000 --- a/.github/workflows/check-branch-alias.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Check Branch Alias - -on: - release: - types: [released] - workflow_dispatch: - -permissions: - contents: write - pull-requests: write - -jobs: - check-branch-alias: - uses: wp-cli/.github/.github/workflows/reusable-check-branch-alias.yml@main diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml deleted file mode 100644 index e9fe57761..000000000 --- a/.github/workflows/code-quality.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Code Quality Checks - -on: - pull_request: - push: - branches: - - main - - master - schedule: - - cron: '17 2 * * *' # Run every day on a seemly random time. - -jobs: - code-quality: - uses: wp-cli/.github/.github/workflows/reusable-code-quality.yml@main diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml deleted file mode 100644 index ffb6f8fd4..000000000 --- a/.github/workflows/copilot-setup-steps.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: "Copilot Setup Steps" - -on: - workflow_dispatch: - push: - paths: - - .github/workflows/copilot-setup-steps.yml - pull_request: - paths: - - .github/workflows/copilot-setup-steps.yml - -permissions: - contents: read - -jobs: - copilot-setup-steps: - name: Setup environment - runs-on: ubuntu-latest - permissions: - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - persist-credentials: false - - - name: Check existence of composer.json file - id: check_composer_file - run: echo "files_exists=$(test -f composer.json && echo true || echo false)" >> "$GITHUB_OUTPUT" - - - name: Set up PHP environment - if: steps.check_composer_file.outputs.files_exists == 'true' - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 - with: - php-version: 'latest' - ini-values: zend.assertions=1, error_reporting=-1, display_errors=On - coverage: 'none' - tools: composer,cs2pr - env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Composer dependencies & cache dependencies - if: steps.check_composer_file.outputs.files_exists == 'true' - uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 - env: - COMPOSER_ROOT_VERSION: dev-${{ github.event.repository.default_branch }} diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml deleted file mode 100644 index 68334703a..000000000 --- a/.github/workflows/issue-triage.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Issue and PR Triage - -'on': - issues: - types: [opened] - pull_request_target: - types: [opened] - workflow_dispatch: - inputs: - issue_number: - description: 'Issue/PR number to triage (leave empty to process all)' - required: false - type: string - -permissions: - issues: write - pull-requests: write - actions: write - contents: read - models: read - -jobs: - issue-triage: - uses: wp-cli/.github/.github/workflows/reusable-issue-triage.yml@main - with: - issue_number: >- - ${{ - (github.event_name == 'workflow_dispatch' && inputs.issue_number) || - (github.event_name == 'pull_request_target' && github.event.pull_request.number) || - (github.event_name == 'issues' && github.event.issue.number) || - '' - }} diff --git a/.github/workflows/manage-labels.yml b/.github/workflows/manage-labels.yml deleted file mode 100644 index 45711bded..000000000 --- a/.github/workflows/manage-labels.yml +++ /dev/null @@ -1,19 +0,0 @@ ---- -name: Manage Labels - -'on': - workflow_dispatch: - push: - branches: - - main - - master - paths: - - 'composer.json' - -permissions: - issues: write - contents: read - -jobs: - manage-labels: - uses: wp-cli/.github/.github/workflows/reusable-manage-labels.yml@main diff --git a/.github/workflows/regenerate-readme.yml b/.github/workflows/regenerate-readme.yml deleted file mode 100644 index 6198d6308..000000000 --- a/.github/workflows/regenerate-readme.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Regenerate README file - -on: - workflow_dispatch: - push: - branches: - - main - - master - paths-ignore: - - "features/**" - - "README.md" - -permissions: - contents: write - pull-requests: write - -jobs: - regenerate-readme: - uses: wp-cli/.github/.github/workflows/reusable-regenerate-readme.yml@main diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml deleted file mode 100644 index bf67592d8..000000000 --- a/.github/workflows/testing.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Testing - -on: - workflow_dispatch: - pull_request: - push: - branches: - - main - - master - schedule: - - cron: '17 1 * * *' # Run every day on a seemly random time. - -jobs: - test: - uses: wp-cli/.github/.github/workflows/reusable-testing.yml@main diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml deleted file mode 100644 index bc01490b3..000000000 --- a/.github/workflows/welcome-new-contributors.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Welcome New Contributors - -on: - pull_request_target: - types: [opened] - branches: - - main - - master - -permissions: - pull-requests: write - -jobs: - welcome: - uses: wp-cli/.github/.github/workflows/reusable-welcome-new-contributors.yml@main diff --git a/.gitignore b/.gitignore index bcf211b32..54f24c800 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,3 @@ node_modules/ vendor/ *.zip *.tar.gz -composer.lock -*.log -phpunit.xml -phpcs.xml -.phpcs.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..697bac1b4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,60 @@ +sudo: false +dist: trusty + +language: php + +notifications: + email: + on_success: never + on_failure: change + +branches: + only: + - master + +cache: + directories: + - $HOME/.composer/cache + +env: + global: + - PATH="$TRAVIS_BUILD_DIR/vendor/bin:$PATH" + - WP_CLI_BIN_DIR="$TRAVIS_BUILD_DIR/vendor/bin" + +matrix: + include: + - php: 7.2 + env: WP_VERSION=latest + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=3.7.11 + - php: 5.6 + env: WP_VERSION=trunk + - php: 5.3 + dist: precise + env: WP_VERSION=latest + +before_install: + - | + # Remove Xdebug for a huge performance increase: + if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then + phpenv config-rm xdebug.ini + else + echo "xdebug.ini does not exist" + fi + +install: + - composer require wp-cli/wp-cli:dev-master + - composer install + - bash bin/install-package-tests.sh + +before_script: + - composer validate + +script: + - bash bin/test.sh diff --git a/.typos.toml b/.typos.toml deleted file mode 100644 index 965f891fc..000000000 --- a/.typos.toml +++ /dev/null @@ -1,6 +0,0 @@ -[default] -extend-ignore-re = [ - "(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", - "(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on", - "(#|//)\\s*spellchecker:ignore-next-line\\n.*" -] diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 1ff84f6d1..000000000 --- a/AGENTS.md +++ /dev/null @@ -1,121 +0,0 @@ -# Instructions - -This package is part of WP-CLI, the official command line interface for WordPress. For a detailed explanation of the project structure and development workflow, please refer to the main @README.md file. - -## Best Practices for Code Contributions - -When contributing to this package, please adhere to the following guidelines: - -* **Follow Existing Conventions:** Before writing any code, analyze the existing codebase in this package to understand the coding style, naming conventions, and architectural patterns. -* **Focus on the Package's Scope:** All changes should be relevant to the functionality of the package. -* **Write Tests:** All new features and bug fixes must be accompanied by acceptance tests using Behat. You can find the existing tests in the `features/` directory. There may be PHPUnit unit tests as well in the `tests/` directory. -* **Update Documentation:** If your changes affect the user-facing functionality, please update the relevant inline code documentation. - -### Building and running - -Before submitting any changes, it is crucial to validate them by running the full suite of static code analysis and tests. To run the full suite of checks, execute the following command: `composer test`. - -This single command ensures that your changes meet all the quality gates of the project. While you can run the individual steps separately, it is highly recommended to use this single command to ensure a comprehensive validation. - -### Useful Composer Commands - -The project uses Composer to manage dependencies and run scripts. The following commands are available: - -* `composer install`: Install dependencies. -* `composer test`: Run the full test suite, including linting, code style checks, static analysis, and unit/behavior tests. -* `composer lint`: Check for syntax errors. -* `composer phpcs`: Check for code style violations. -* `composer phpcbf`: Automatically fix code style violations. -* `composer phpstan`: Run static analysis. -* `composer phpunit`: Run unit tests. -* `composer behat`: Run behavior-driven tests. - -### Coding Style - -The project follows the `WP_CLI_CS` coding standard, which is enforced by PHP_CodeSniffer. The configuration can be found in `phpcs.xml.dist`. Before submitting any code, please run `composer phpcs` to check for violations and `composer phpcbf` to automatically fix them. - -## Documentation - -The `README.md` file might be generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). In that case, changes need to be made against the corresponding part of the codebase. - -### Inline Documentation - -Only write high-value comments if at all. Avoid talking to the user through comments. - -## Testing - -The project has a comprehensive test suite that includes unit tests, behavior-driven tests, and static analysis. - -* **Unit tests** are written with PHPUnit and can be found in the `tests/` directory. The configuration is in `phpunit.xml.dist`. -* **Behavior-driven tests** are written with Behat and can be found in the `features/` directory. The configuration is in `behat.yml`. -* **Static analysis** is performed with PHPStan. - -All tests are run on GitHub Actions for every pull request. - -When writing tests, aim to follow existing patterns. Key conventions include: - -* When adding tests, first examine existing tests to understand and conform to established conventions. -* For unit tests, extend the base `WP_CLI\Tests\TestCase` test class. -* For Behat tests, only WP-CLI commands installed in `composer.json` can be run. - -### Behat Steps - -WP-CLI makes use of a Behat-based testing framework and provides a set of custom step definitions to write feature tests. - -> **Note:** If you are expecting an error output in a test, you need to use `When I try ...` instead of `When I run ...` . - -#### Given - -* `Given an empty directory` - Creates an empty directory. -* `Given /^an? (empty|non-existent) ([^\s]+) directory$/` - Creates or deletes a specific directory. -* `Given an empty cache` - Clears the WP-CLI cache directory. -* `Given /^an? ([^\s]+) (file|cache file):$/` - Creates a file with the given contents. -* `Given /^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/` - Search and replace a string in a file using regex. -* `Given /^that HTTP requests to (.*?) will respond with:$/` - Mock HTTP requests to a given URL. -* `Given WP files` - Download WordPress files without installing. -* `Given wp-config.php` - Create a wp-config.php file using `wp config create`. -* `Given a database` - Creates an empty database. -* `Given a WP install(ation)` - Installs WordPress. -* `Given a WP install(ation) in :subdir` - Installs WordPress in a given directory. -* `Given a WP install(ation) with Composer` - Installs WordPress with Composer. -* `Given a WP install(ation) with Composer and a custom vendor directory :vendor_directory` - Installs WordPress with Composer and a custom vendor directory. -* `Given /^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/` - Installs WordPress Multisite. -* `Given these installed and active plugins:` - Installs and activates one or more plugins. -* `Given a custom wp-content directory` - Configure a custom `wp-content` directory. -* `Given download:` - Download multiple files into the given destinations. -* `Given /^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/` - Store STDOUT or STDERR contents in a variable. -* `Given /^a new Phar with (?:the same version|version "([^"]+)")$/` - Build a new WP-CLI Phar file with a given version. -* `Given /^a downloaded Phar with (?:the same version|version "([^"]+)")$/` - Download a specific WP-CLI Phar version from GitHub. -* `Given /^save the (.+) file ([\'].+[^\'])? as \{(\w+)\}$/` - Stores the contents of the given file in a variable. -* `Given a misconfigured WP_CONTENT_DIR constant directory` - Modify wp-config.php to set `WP_CONTENT_DIR` to an empty string. -* `Given a dependency on current wp-cli` - Add `wp-cli/wp-cli` as a Composer dependency. -* `Given a PHP built-in web server` - Start a PHP built-in web server in the current directory. -* `Given a PHP built-in web server to serve :subdir` - Start a PHP built-in web server in the given subdirectory. - -#### When - -* ``When /^I launch in the background `([^`]+)`$/`` - Launch a given command in the background. -* ``When /^I (run|try) `([^`]+)`$/`` - Run or try a given command. -* ``When /^I (run|try) `([^`]+)` from '([^\s]+)'$/`` - Run or try a given command in a subdirectory. -* `When /^I (run|try) the previous command again$/` - Run or try the previous command again. - -#### Then - -* `Then /^the return code should( not)? be (\d+)$/` - Expect a specific exit code of the previous command. -* `Then /^(STDOUT|STDERR) should( strictly)? (be|contain|not contain):$/` - Check the contents of STDOUT or STDERR. -* `Then /^(STDOUT|STDERR) should be a number$/` - Expect STDOUT or STDERR to be a numeric value. -* `Then /^(STDOUT|STDERR) should not be a number$/` - Expect STDOUT or STDERR to not be a numeric value. -* `Then /^STDOUT should be a table containing rows:$/` - Expect STDOUT to be a table containing the given rows. -* `Then /^STDOUT should end with a table containing rows:$/` - Expect STDOUT to end with a table containing the given rows. -* `Then /^STDOUT should be JSON containing:$/` - Expect valid JSON output in STDOUT. -* `Then /^STDOUT should be a JSON array containing:$/` - Expect valid JSON array output in STDOUT. -* `Then /^STDOUT should be CSV containing:$/` - Expect STDOUT to be CSV containing certain values. -* `Then /^STDOUT should be YAML containing:$/` - Expect STDOUT to be YAML containing certain content. -* `Then /^(STDOUT|STDERR) should be empty$/` - Expect STDOUT or STDERR to be empty. -* `Then /^(STDOUT|STDERR) should not be empty$/` - Expect STDOUT or STDERR not to be empty. -* `Then /^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|<>) ([+\w.{}-]+)$/` - Expect STDOUT or STDERR to be a version string comparing to the given version. -* `Then /^the (.+) (file|directory) should( strictly)? (exist|not exist|be:|contain:|not contain):$/` - Expect a certain file or directory to (not) exist or (not) contain certain contents. -* `Then /^the contents of the (.+) file should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match file contents against a regex. -* `Then /^(STDOUT|STDERR) should( not)? match (((\/.*\/)|(#.#))([a-z]+)?)$/` - Match STDOUT or STDERR against a regex. -* `Then /^an email should (be sent|not be sent)$/` - Expect an email to be sent (or not). -* `Then the HTTP status code should be :code` - Expect the HTTP status code for visiting `http://localhost:8080`. diff --git a/LICENSE b/LICENSE index b9e0bc993..b84d27915 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (C) 2011-2018 WP-CLI Development Group (https://github.com/wp-cli/cache-command/contributors) +Copyright (C) 2011-2017 WP-CLI Development Group (https://github.com/wp-cli/config-command/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index ec584ec7e..d347ea3d6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ wp-cli/cache-command Manages object and transient caches. -[![Testing](https://github.com/wp-cli/cache-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/cache-command/actions/workflows/testing.yml) [![Code Coverage](https://codecov.io/gh/wp-cli/cache-command/branch/main/graph/badge.svg)](https://codecov.io/gh/wp-cli/cache-command/tree/main) +[![Build Status](https://travis-ci.org/wp-cli/cache-command.svg?branch=master)](https://travis-ci.org/wp-cli/cache-command) Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) @@ -23,7 +23,7 @@ By default, the WP Object Cache exists in PHP memory for the length of the request (and is emptied at the end). Use a persistent object cache drop-in to persist the object cache between requests. -[Read the documentation](https://developer.wordpress.org/reference/classes/wp_object_cache) +[Read the codex article](https://codex.wordpress.org/Class_Reference/WP_Object_Cache) for more detail. **EXAMPLES** @@ -72,8 +72,8 @@ be added. **EXAMPLES** # Add cache. - $ wp cache add my_key my_value my_group 300 - Success: Added object 'my_key' in group 'my_group'. + $ wp cache add my_key my_group my_value 300 + Success: Added object 'my_key' in group 'my_value'. @@ -164,27 +164,6 @@ Errors if the object cache can't be flushed. -### wp cache flush-group - -Removes all cache items in a group, if the object cache implementation supports it. - -~~~ -wp cache flush-group -~~~ - -**OPTIONS** - - - Cache group key. - -**EXAMPLES** - - # Clear cache group. - $ wp cache flush-group my_group - Success: Cache group 'my_group' was flushed. - - - ### wp cache get Gets a value from the object cache. @@ -249,91 +228,6 @@ Errors if the value can't be incremented. -### wp cache patch - -Update a nested value from the cache. - -~~~ -wp cache patch ... [] [--group=] [--expiration=] [--format=] -~~~ - -**OPTIONS** - - - Patch action to perform. - --- - options: - - insert - - update - - delete - --- - - - Cache key. - - ... - The name(s) of the keys within the value to locate the value to patch. - - [] - The new value. If omitted, the value is read from STDIN. - - [--group=] - Method for grouping data within the cache which allows the same key to be used across groups. - --- - default: default - --- - - [--expiration=] - Define how long to keep the value, in seconds. `0` means as long as possible. - --- - default: 0 - --- - - [--format=] - The serialization format for the value. - --- - default: plaintext - options: - - plaintext - - json - --- - - - -### wp cache pluck - -Get a nested value from the cache. - -~~~ -wp cache pluck ... [--group=] [--format=] -~~~ - -**OPTIONS** - - - Cache key. - - ... - The name(s) of the keys within the value to locate the value to pluck. - - [--group=] - Method for grouping data within the cache which allows the same key to be used across groups. - --- - default: default - --- - - [--format=] - The output format of the value. - --- - default: plaintext - options: - - plaintext - - json - - yaml - --- - - - ### wp cache replace Replaces a value in the object cache, if the value already exists. @@ -410,33 +304,6 @@ Errors if the value can't be set. -### wp cache supports - -Determines whether the object cache implementation supports a particular feature. - -~~~ -wp cache supports -~~~ - -**OPTIONS** - - - Name of the feature to check for. - -**EXAMPLES** - - # Check whether is add_multiple supported. - $ wp cache supports add_multiple - $ echo $? - 0 - - # Bash script for checking whether for support like this: - if ! wp cache supports non_existing; then - echo 'non_existing is not supported' - fi - - - ### wp cache type Attempts to determine which object cache is being used. @@ -497,10 +364,6 @@ the transient cache skips the database and simply wraps the WP Object Cache. $ wp transient delete --all Success: 14 transients deleted from the database. - # Delete all site transients. - $ wp transient delete --all --network - Success: 2 transients deleted from the database. - ### wp transient delete @@ -540,21 +403,10 @@ network|site cache, please see docs for `wp transient`. $ wp transient delete --expired Success: 12 expired transients deleted from the database. - # Delete expired site transients. - $ wp transient delete --expired --network - Success: 1 expired transient deleted from the database. - # Delete all transients. $ wp transient delete --all Success: 14 transients deleted from the database. - # Delete all site transients. - $ wp transient delete --all --network - Success: 2 transients deleted from the database. - - # Delete all transients in a multisite. - $ wp transient delete --all --network && wp site list --field=url | xargs -n1 -I % wp --url=% transient delete --all - ### wp transient get @@ -599,154 +451,6 @@ network|site cache, please see docs for `wp transient`. -### wp transient list - -Lists transients and their values. - -~~~ -wp transient list [--search=] [--exclude=] [--network] [--unserialize] [--human-readable] [--fields=] [--format=] -~~~ - -**OPTIONS** - - [--search=] - Use wildcards ( * and ? ) to match transient name. - - [--exclude=] - Pattern to exclude. Use wildcards ( * and ? ) to match transient name. - - [--network] - Get the values of network|site transients. On single site, this is - a specially-named cache key. On multisite, this is a global cache - (instead of local to the site). - - [--unserialize] - Unserialize transient values in output. - - [--human-readable] - Human-readable output for expirations. - - [--fields=] - Limit the output to specific object fields. - - [--format=] - The serialization format for the value. - --- - default: table - options: - - table - - json - - csv - - count - - yaml - --- - -**AVAILABLE FIELDS** - -This field will be displayed by default for each matching option: - -* name -* value -* expiration - -**EXAMPLES** - - # List all transients - $ wp transient list - +------+-------+---------------+ - | name | value | expiration | - +------+-------+---------------+ - | foo | bar | 39 mins | - | foo2 | bar2 | no expiration | - | foo3 | bar2 | expired | - | foo4 | bar4 | 4 hours | - +------+-------+---------------+ - - - -### wp transient patch - -Update a nested value from a transient. - -~~~ -wp transient patch ... [] [--format=] [--expiration=] [--network] -~~~ - -**OPTIONS** - - - Patch action to perform. - --- - options: - - insert - - update - - delete - --- - - - Key for the transient. - - ... - The name(s) of the keys within the value to locate the value to patch. - - [] - The new value. If omitted, the value is read from STDIN. - - [--format=] - The serialization format for the value. - --- - default: plaintext - options: - - plaintext - - json - --- - - [--expiration=] - Time until expiration, in seconds. - --- - default: 0 - --- - - [--network] - Get the value of a network|site transient. On single site, this is - a specially-named cache key. On multisite, this is a global cache - (instead of local to the site). - - - -### wp transient pluck - -Get a nested value from a transient. - -~~~ -wp transient pluck ... [--format=] [--network] -~~~ - -**OPTIONS** - - - Key for the transient. - - ... - The name(s) of the keys within the value to locate the value to pluck. - - [--format=] - The output format of the value. - --- - default: plaintext - options: - - plaintext - - json - - yaml - --- - - [--network] - Get the value of a network|site transient. On single site, this is - a specially-named cache key. On multisite, this is a global cache - (instead of local to the site). - - - ### wp transient set Sets a transient value. @@ -792,7 +496,7 @@ wp transient type ~~~ Indicates whether the transients API is using an object cache or the -database. +options table. For a more complete explanation of the transient cache, including the network|site cache, please see docs for `wp transient`. @@ -800,7 +504,7 @@ network|site cache, please see docs for `wp transient`. **EXAMPLES** $ wp transient type - Transients are saved to the database. + Transients are saved to the wp_options table. ## Installing @@ -832,13 +536,9 @@ Want to contribute a new feature? Please first [open a new issue](https://github Once you've decided to commit the time to seeing your pull request through, [please follow our guidelines for creating a pull request](https://make.wordpress.org/cli/handbook/pull-requests/) to make sure it's a pleasant experience. See "[Setting up](https://make.wordpress.org/cli/handbook/pull-requests/#setting-up)" for details specific to working on this package locally. -### License - -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. - ## Support -GitHub issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support +Github issues aren't for general support questions, but there are other venues you can try: https://wp-cli.org/#support *This README.md is generated dynamically from the project's codebase using `wp scaffold package-readme` ([doc](https://github.com/wp-cli/scaffold-package-command#wp-scaffold-package-readme)). To suggest changes, please submit a pull request against the corresponding part of the codebase.* diff --git a/behat.yml b/behat.yml deleted file mode 100644 index d6ee86224..000000000 --- a/behat.yml +++ /dev/null @@ -1,7 +0,0 @@ -default: - suites: - default: - contexts: - - WP_CLI\Tests\Context\FeatureContext - paths: - - features diff --git a/bin/install-package-tests.sh b/bin/install-package-tests.sh new file mode 100755 index 000000000..d672d9a50 --- /dev/null +++ b/bin/install-package-tests.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -ex + +install_db() { + mysql -e 'CREATE DATABASE IF NOT EXISTS wp_cli_test;' -uroot + mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot + mysql -e 'GRANT ALL PRIVILEGES ON wp_cli_test_scaffold.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1"' -uroot +} + +install_db diff --git a/bin/test.sh b/bin/test.sh new file mode 100644 index 000000000..bd3ae6e91 --- /dev/null +++ b/bin/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -ex + +# Run the unit tests, if they exist +if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ] +then + phpunit +fi + +# Run the functional tests +BEHAT_TAGS=$(php utils/behat-tags.php) +behat --format progress $BEHAT_TAGS --strict diff --git a/cache-command.php b/cache-command.php index 338e02219..354a587c4 100644 --- a/cache-command.php +++ b/cache-command.php @@ -4,9 +4,9 @@ return; } -$wpcli_cache_autoloader = __DIR__ . '/vendor/autoload.php'; -if ( file_exists( $wpcli_cache_autoloader ) ) { - require_once $wpcli_cache_autoloader; +$autoload = dirname( __FILE__ ) . '/vendor/autoload.php'; +if ( file_exists( $autoload ) ) { + require_once $autoload; } WP_CLI::add_command( 'cache', 'Cache_Command' ); diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/composer.json b/composer.json index aba06a053..cc65aa45b 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,11 @@ { "name": "wp-cli/cache-command", - "type": "wp-cli-package", "description": "Manages object and transient caches.", + "type": "wp-cli-package", "homepage": "https://github.com/wp-cli/cache-command", + "support": { + "issues": "https://github.com/wp-cli/cache-command/issues" + }, "license": "MIT", "authors": [ { @@ -11,26 +14,22 @@ "homepage": "https://runcommand.io" } ], - "require": { - "wp-cli/wp-cli": "^2.13" + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "": "src/" + }, + "files": [ "cache-command.php" ] }, + "require": {}, "require-dev": { - "wp-cli/entity-command": "^1.3 || ^2", - "wp-cli/wp-cli-tests": "^5" - }, - "config": { - "process-timeout": 7200, - "sort-packages": true, - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true, - "johnpbloch/wordpress-core-installer": true, - "phpstan/extension-installer": true - }, - "lock": false + "behat/behat": "~2.5", + "wp-cli/wp-cli": "*" }, "extra": { "branch-alias": { - "dev-main": "2.x-dev" + "dev-master": "1.x-dev" }, "bundled": true, "commands": [ @@ -39,53 +38,16 @@ "cache decr", "cache delete", "cache flush", - "cache flush-group", "cache get", "cache incr", - "cache patch", - "cache pluck", "cache replace", "cache set", - "cache supports", "cache type", "transient", "transient delete", "transient get", - "transient list", - "transient patch", - "transient pluck", "transient set", "transient type" ] - }, - "autoload": { - "classmap": [ - "src/" - ], - "files": [ - "cache-command.php" - ] - }, - "minimum-stability": "dev", - "prefer-stable": true, - "scripts": { - "behat": "run-behat-tests", - "behat-rerun": "rerun-behat-tests", - "lint": "run-linter-tests", - "phpcs": "run-phpcs-tests", - "phpstan": "run-phpstan-tests", - "phpcbf": "run-phpcbf-cleanup", - "phpunit": "run-php-unit-tests", - "prepare-tests": "install-package-tests", - "test": [ - "@lint", - "@phpcs", - "@phpstan", - "@phpunit", - "@behat" - ] - }, - "support": { - "issues": "https://github.com/wp-cli/cache-command/issues" } } diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..b74d7668e --- /dev/null +++ b/features/bootstrap/FeatureContext.php @@ -0,0 +1,903 @@ +autoload->files ) ) { + $contents = 'require:' . PHP_EOL; + foreach( $composer->autoload->files as $file ) { + $contents .= ' - ' . dirname( dirname( dirname( __FILE__ ) ) ) . '/' . $file . PHP_EOL; + } + @mkdir( sys_get_temp_dir() . '/wp-cli-package-test/' ); + $project_config = sys_get_temp_dir() . '/wp-cli-package-test/config.yml'; + file_put_contents( $project_config, $contents ); + putenv( 'WP_CLI_CONFIG_PATH=' . $project_config ); + } + } +// Inside WP-CLI +} else { + require_once __DIR__ . '/../../php/utils.php'; + require_once __DIR__ . '/../../php/WP_CLI/Process.php'; + require_once __DIR__ . '/../../php/WP_CLI/ProcessRun.php'; + if ( file_exists( __DIR__ . '/../../vendor/autoload.php' ) ) { + require_once __DIR__ . '/../../vendor/autoload.php'; + } else if ( file_exists( __DIR__ . '/../../../../autoload.php' ) ) { + require_once __DIR__ . '/../../../../autoload.php'; + } +} + +/** + * Features context. + */ +class FeatureContext extends BehatContext implements ClosuredContextInterface { + + /** + * The current working directory for scenarios that have a "Given a WP installation" or "Given an empty directory" step. Variable RUN_DIR. Lives until the end of the scenario. + */ + private static $run_dir; + + /** + * Where WordPress core is downloaded to for caching, and which is copied to RUN_DIR during a "Given a WP installation" step. Lives until manually deleted. + */ + private static $cache_dir; + + /** + * The directory that holds the install cache, and which is copied to RUN_DIR during a "Given a WP installation" step. Recreated on each suite run. + */ + private static $install_cache_dir; + + /** + * The directory that the WP-CLI cache (WP_CLI_CACHE_DIR, normally "$HOME/.wp-cli/cache") is set to on a "Given an empty cache" step. + * Variable SUITE_CACHE_DIR. Lives until the end of the scenario (or until another "Given an empty cache" step within the scenario). + */ + private static $suite_cache_dir; + + /** + * Where the current WP-CLI source repository is copied to for Composer-based tests with a "Given a dependency on current wp-cli" step. + * Variable COMPOSER_LOCAL_REPOSITORY. Lives until the end of the suite. + */ + private static $composer_local_repository; + + /** + * The test database settings. All but `dbname` can be set via environment variables. The database is dropped at the start of each scenario and created on a "Given a WP installation" step. + */ + private static $db_settings = array( + 'dbname' => 'wp_cli_test', + 'dbuser' => 'wp_cli_test', + 'dbpass' => 'password1', + 'dbhost' => '127.0.0.1', + ); + + /** + * Array of background process ids started by the current scenario. Used to terminate them at the end of the scenario. + */ + private $running_procs = array(); + + /** + * Array of variables available as {VARIABLE_NAME}. Some are always set: CORE_CONFIG_SETTINGS, SRC_DIR, CACHE_DIR, WP_VERSION-version-latest. Some are step-dependent: + * RUN_DIR, SUITE_CACHE_DIR, COMPOSER_LOCAL_REPOSITORY, PHAR_PATH. Scenarios can define their own variables using "Given save" steps. Variables are reset for each scenario. + */ + public $variables = array(); + + /** + * The current feature file and scenario line number as '.'. Used in RUN_DIR and SUITE_CACHE_DIR directory names. Set at the start of each scenario. + */ + private static $temp_dir_infix; + + /** + * Settings and variables for WP_CLI_TEST_LOG_RUN_TIMES run time logging. + */ + private static $log_run_times; // Whether to log run times - WP_CLI_TEST_LOG_RUN_TIMES env var. Set on `@BeforeScenario'. + private static $suite_start_time; // When the suite started, set on `@BeforeScenario'. + private static $output_to; // Where to output log - stdout|error_log. Set on `@BeforeSuite`. + private static $num_top_processes; // Number of processes/methods to output by longest run times. Set on `@BeforeSuite`. + private static $num_top_scenarios; // Number of scenarios to output by longest run times. Set on `@BeforeSuite`. + + private static $scenario_run_times = array(); // Scenario run times (top `self::$num_top_scenarios` only). + private static $scenario_count = 0; // Scenario count, incremented on `@AfterScenario`. + private static $proc_method_run_times = array(); // Array of run time info for proc methods, keyed by method name and arg, each a 2-element array containing run time and run count. + + /** + * Get the environment variables required for launched `wp` processes + */ + private static function get_process_env_variables() { + // Ensure we're using the expected `wp` binary + $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ?: realpath( __DIR__ . '/../../bin' ); + $vendor_dir = realpath( __DIR__ . '/../../vendor/bin' ); + $env = array( + 'PATH' => $bin_dir . ':' . $vendor_dir . ':' . getenv( 'PATH' ), + 'BEHAT_RUN' => 1, + 'HOME' => sys_get_temp_dir() . '/wp-cli-home', + ); + if ( $config_path = getenv( 'WP_CLI_CONFIG_PATH' ) ) { + $env['WP_CLI_CONFIG_PATH'] = $config_path; + } + if ( $term = getenv( 'TERM' ) ) { + $env['TERM'] = $term; + } + if ( $php_args = getenv( 'WP_CLI_PHP_ARGS' ) ) { + $env['WP_CLI_PHP_ARGS'] = $php_args; + } + if ( $php_used = getenv( 'WP_CLI_PHP_USED' ) ) { + $env['WP_CLI_PHP_USED'] = $php_used; + } + if ( $php = getenv( 'WP_CLI_PHP' ) ) { + $env['WP_CLI_PHP'] = $php; + } + if ( $travis_build_dir = getenv( 'TRAVIS_BUILD_DIR' ) ) { + $env['TRAVIS_BUILD_DIR'] = $travis_build_dir; + } + if ( $github_token = getenv( 'GITHUB_TOKEN' ) ) { + $env['GITHUB_TOKEN'] = $github_token; + } + return $env; + } + + /** + * We cache the results of `wp core download` to improve test performance. + * Ideally, we'd cache at the HTTP layer for more reliable tests. + */ + private static function cache_wp_files() { + $wp_version_suffix = ( $wp_version = getenv( 'WP_VERSION' ) ) ? "-$wp_version" : ''; + self::$cache_dir = sys_get_temp_dir() . '/wp-cli-test-core-download-cache' . $wp_version_suffix; + + if ( is_readable( self::$cache_dir . '/wp-config-sample.php' ) ) + return; + + $cmd = Utils\esc_cmd( 'wp core download --force --path=%s', self::$cache_dir ); + if ( $wp_version ) { + $cmd .= Utils\esc_cmd( ' --version=%s', $wp_version ); + } + Process::create( $cmd, null, self::get_process_env_variables() )->run_check(); + } + + /** + * @BeforeSuite + */ + public static function prepare( SuiteEvent $event ) { + // Test performance statistics - useful for detecting slow tests. + if ( self::$log_run_times = getenv( 'WP_CLI_TEST_LOG_RUN_TIMES' ) ) { + self::log_run_times_before_suite( $event ); + } + + $result = Process::create( 'wp cli info', null, self::get_process_env_variables() )->run_check(); + echo PHP_EOL; + echo $result->stdout; + echo PHP_EOL; + self::cache_wp_files(); + $result = Process::create( Utils\esc_cmd( 'wp core version --path=%s', self::$cache_dir ) , null, self::get_process_env_variables() )->run_check(); + echo 'WordPress ' . $result->stdout; + echo PHP_EOL; + + // Remove install cache if any (not setting the static var). + $wp_version_suffix = ( $wp_version = getenv( 'WP_VERSION' ) ) ? "-$wp_version" : ''; + $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 ); + } + } + + /** + * @AfterSuite + */ + public static function afterSuite( SuiteEvent $event ) { + if ( self::$composer_local_repository ) { + self::remove_dir( self::$composer_local_repository ); + self::$composer_local_repository = null; + } + + if ( self::$log_run_times ) { + self::log_run_times_after_suite( $event ); + } + } + + /** + * @BeforeScenario + */ + public function beforeScenario( $event ) { + if ( self::$log_run_times ) { + self::log_run_times_before_scenario( $event ); + } + + $this->variables['SRC_DIR'] = realpath( __DIR__ . '/../..' ); + + // Used in the names of the RUN_DIR and SUITE_CACHE_DIR directories. + self::$temp_dir_infix = null; + if ( $file = self::get_event_file( $event, $line ) ) { + self::$temp_dir_infix = basename( $file ) . '.' . $line; + } + } + + /** + * @AfterScenario + */ + public function afterScenario( $event ) { + + if ( self::$run_dir ) { + // remove altered WP install, unless there's an error + if ( $event->getResult() < 4 ) { + self::remove_dir( self::$run_dir ); + } + self::$run_dir = null; + } + + // Remove WP-CLI package directory if any. Set to `wp package path` by package-command and scaffold-package-command features, and by cli-info.feature. + if ( isset( $this->variables['PACKAGE_PATH'] ) ) { + self::remove_dir( $this->variables['PACKAGE_PATH'] ); + } + + // Remove SUITE_CACHE_DIR if any. + if ( self::$suite_cache_dir ) { + self::remove_dir( self::$suite_cache_dir ); + self::$suite_cache_dir = null; + } + + // Remove any background processes. + foreach ( $this->running_procs as $proc ) { + $status = proc_get_status( $proc ); + self::terminate_proc( $status['pid'] ); + } + + if ( self::$log_run_times ) { + self::log_run_times_after_scenario( $event ); + } + } + + /** + * Terminate a process and any of its children. + */ + private static function terminate_proc( $master_pid ) { + + $output = `ps -o ppid,pid,command | grep $master_pid`; + + foreach ( explode( PHP_EOL, $output ) as $line ) { + if ( preg_match( '/^\s*(\d+)\s+(\d+)/', $line, $matches ) ) { + $parent = $matches[1]; + $child = $matches[2]; + + if ( $parent == $master_pid ) { + self::terminate_proc( $child ); + } + } + } + + if ( ! posix_kill( (int) $master_pid, 9 ) ) { + $errno = posix_get_last_error(); + // Ignore "No such process" error as that's what we want. + if ( 3 /*ESRCH*/ !== $errno ) { + throw new RuntimeException( posix_strerror( $errno ) ); + } + } + } + + /** + * Create a temporary WP_CLI_CACHE_DIR. Exposed as SUITE_CACHE_DIR in "Given an empty cache" step. + */ + public static function create_cache_dir() { + 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 ); + mkdir( self::$suite_cache_dir ); + return self::$suite_cache_dir; + } + + /** + * Initializes context. + * Every scenario gets its own context object. + * + * @param array $parameters context parameters (set them up through behat.yml) + */ + public function __construct( array $parameters ) { + if ( getenv( 'WP_CLI_TEST_DBUSER' ) ) { + self::$db_settings['dbuser'] = getenv( 'WP_CLI_TEST_DBUSER' ); + } + + if ( false !== getenv( 'WP_CLI_TEST_DBPASS' ) ) { + self::$db_settings['dbpass'] = getenv( 'WP_CLI_TEST_DBPASS' ); + } + + if ( getenv( 'WP_CLI_TEST_DBHOST' ) ) { + self::$db_settings['dbhost'] = getenv( 'WP_CLI_TEST_DBHOST' ); + } + + $this->drop_db(); + $this->set_cache_dir(); + $this->variables['CORE_CONFIG_SETTINGS'] = Utils\assoc_args_to_str( self::$db_settings ); + } + + public function getStepDefinitionResources() { + return glob( __DIR__ . '/../steps/*.php' ); + } + + public function getHookDefinitionResources() { + return array(); + } + + /** + * Replace {VARIABLE_NAME}. Note that variable names can only contain uppercase letters and underscores (no numbers). + */ + public function replace_variables( $str ) { + $ret = preg_replace_callback( '/\{([A-Z_]+)\}/', array( $this, '_replace_var' ), $str ); + if ( false !== strpos( $str, '{WP_VERSION-' ) ) { + $ret = $this->_replace_wp_versions( $ret ); + } + return $ret; + } + + /** + * Replace variables callback. + */ + private function _replace_var( $matches ) { + $cmd = $matches[0]; + + foreach ( array_slice( $matches, 1 ) as $key ) { + $cmd = str_replace( '{' . $key . '}', $this->variables[ $key ], $cmd ); + } + + return $cmd; + } + + /** + * Substitute "{WP_VERSION-version-latest}" variables. + */ + private function _replace_wp_versions( $str ) { + static $wp_versions = null; + if ( null === $wp_versions ) { + $wp_versions = array(); + + $response = Requests::get( 'https://api.wordpress.org/core/version-check/1.7/', null, array( 'timeout' => 30 ) ); + if ( 200 === $response->status_code && ( $body = json_decode( $response->body ) ) && is_object( $body ) && isset( $body->offers ) && is_array( $body->offers ) ) { + // Latest version alias. + $wp_versions["{WP_VERSION-latest}"] = count( $body->offers ) ? $body->offers[0]->version : ''; + foreach ( $body->offers as $offer ) { + $sub_ver = preg_replace( '/(^[0-9]+\.[0-9]+)\.[0-9]+$/', '$1', $offer->version ); + $sub_ver_key = "{WP_VERSION-{$sub_ver}-latest}"; + + $main_ver = preg_replace( '/(^[0-9]+)\.[0-9]+$/', '$1', $sub_ver ); + $main_ver_key = "{WP_VERSION-{$main_ver}-latest}"; + + if ( ! isset( $wp_versions[ $main_ver_key ] ) ) { + $wp_versions[ $main_ver_key ] = $offer->version; + } + if ( ! isset( $wp_versions[ $sub_ver_key ] ) ) { + $wp_versions[ $sub_ver_key ] = $offer->version; + } + } + } + } + return strtr( $str, $wp_versions ); + } + + /** + * Get the file and line number for the current behat event. + */ + private static function get_event_file( $event, &$line ) { + if ( method_exists( $event, 'getScenario' ) ) { + $scenario_feature = $event->getScenario(); + } elseif ( method_exists( $event, 'getFeature' ) ) { + $scenario_feature = $event->getFeature(); + } elseif ( method_exists( $event, 'getOutline' ) ) { + $scenario_feature = $event->getOutline(); + } else { + return null; + } + $line = $scenario_feature->getLine(); + return $scenario_feature->getFile(); + } + + /** + * Create the RUN_DIR directory, unless already set for this scenario. + */ + public function create_run_dir() { + if ( !isset( $this->variables['RUN_DIR'] ) ) { + self::$run_dir = $this->variables['RUN_DIR'] = sys_get_temp_dir() . '/' . uniqid( 'wp-cli-test-run-' . self::$temp_dir_infix . '-', TRUE ); + mkdir( $this->variables['RUN_DIR'] ); + } + } + + public function build_phar( $version = 'same' ) { + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' . uniqid( "wp-cli-build-", TRUE ) . '.phar'; + + // Test running against a package installed as a WP-CLI dependency + // WP-CLI installed as a project dependency + $make_phar_path = __DIR__ . '/../../../../../utils/make-phar.php'; + if ( ! file_exists( $make_phar_path ) ) { + // Test running against WP-CLI proper + $make_phar_path = __DIR__ . '/../../utils/make-phar.php'; + if ( ! file_exists( $make_phar_path ) ) { + // WP-CLI as a dependency of this project + $make_phar_path = __DIR__ . '/../../vendor/wp-cli/wp-cli/utils/make-phar.php'; + } + } + + $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(); + } + + public function download_phar( $version = 'same' ) { + if ( 'same' === $version ) { + $version = WP_CLI_VERSION; + } + + $download_url = sprintf( + 'https://github.com/wp-cli/wp-cli/releases/download/v%1$s/wp-cli-%1$s.phar', + $version + ); + + $this->variables['PHAR_PATH'] = $this->variables['RUN_DIR'] . '/' + . uniqid( 'wp-cli-download-', true ) + . '.phar'; + + Process::create( Utils\esc_cmd( + 'curl -sSfL %1$s > %2$s && chmod +x %2$s', + $download_url, + $this->variables['PHAR_PATH'] + ) )->run_check(); + } + + /** + * CACHE_DIR is a cache for downloaded test data such as images. Lives until manually deleted. + */ + private function set_cache_dir() { + $path = sys_get_temp_dir() . '/wp-cli-test-cache'; + if ( ! file_exists( $path ) ) { + mkdir( $path ); + } + $this->variables['CACHE_DIR'] = $path; + } + + /** + * Run a MySQL command with `$db_settings`. + * + * @param string $sql_cmd Command to run. + * @param array $assoc_args Optional. Associative array of options. Default empty. + * @param bool $add_database Optional. Whether to add dbname to the $sql_cmd. Default false. + */ + private static function run_sql( $sql_cmd, $assoc_args = array(), $add_database = false ) { + $default_assoc_args = array( + 'host' => self::$db_settings['dbhost'], + 'user' => self::$db_settings['dbuser'], + 'pass' => self::$db_settings['dbpass'], + ); + if ( $add_database ) { + $sql_cmd .= ' ' . escapeshellarg( self::$db_settings['dbname'] ); + } + $start_time = microtime( true ); + Utils\run_mysql_command( $sql_cmd, array_merge( $assoc_args, $default_assoc_args ) ); + if ( self::$log_run_times ) { + self::log_proc_method_run_time( 'run_sql ' . $sql_cmd, $start_time ); + } + } + + public function create_db() { + $dbname = self::$db_settings['dbname']; + self::run_sql( 'mysql --no-defaults', array( 'execute' => "CREATE DATABASE IF NOT EXISTS $dbname" ) ); + } + + public function drop_db() { + $dbname = self::$db_settings['dbname']; + self::run_sql( 'mysql --no-defaults', array( 'execute' => "DROP DATABASE IF EXISTS $dbname" ) ); + } + + public function proc( $command, $assoc_args = array(), $path = '' ) { + if ( !empty( $assoc_args ) ) + $command .= Utils\assoc_args_to_str( $assoc_args ); + + $env = self::get_process_env_variables(); + if ( isset( $this->variables['SUITE_CACHE_DIR'] ) ) { + $env['WP_CLI_CACHE_DIR'] = $this->variables['SUITE_CACHE_DIR']; + } + + if ( isset( $this->variables['RUN_DIR'] ) ) { + $cwd = "{$this->variables['RUN_DIR']}/{$path}"; + } else { + $cwd = null; + } + + return Process::create( $command, $cwd, $env ); + } + + /** + * Start a background process. Will automatically be closed when the tests finish. + */ + public function background_proc( $cmd ) { + $descriptors = array( + 0 => STDIN, + 1 => array( 'pipe', 'w' ), + 2 => array( 'pipe', 'w' ), + ); + + $proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() ); + + sleep(1); + + $status = proc_get_status( $proc ); + + if ( !$status['running'] ) { + $stderr = is_resource( $pipes[2] ) ? ( ': ' . stream_get_contents( $pipes[2] ) ) : ''; + throw new RuntimeException( sprintf( "Failed to start background process '%s'%s.", $cmd, $stderr ) ); + } else { + $this->running_procs[] = $proc; + } + } + + public function move_files( $src, $dest ) { + rename( $this->variables['RUN_DIR'] . "/$src", $this->variables['RUN_DIR'] . "/$dest" ); + } + + /** + * Remove a directory (recursive). + */ + public static function remove_dir( $dir ) { + Process::create( Utils\esc_cmd( 'rm -rf %s', $dir ) )->run_check(); + } + + /** + * Copy a directory (recursive). Destination directory must exist. + */ + public static function copy_dir( $src_dir, $dest_dir ) { + Process::create( Utils\esc_cmd( "cp -r %s/* %s", $src_dir, $dest_dir ) )->run_check(); + } + + public function add_line_to_wp_config( &$wp_config_code, $line ) { + $token = "/* That's all, stop editing!"; + + $wp_config_code = str_replace( $token, "$line\n\n$token", $wp_config_code ); + } + + public function download_wp( $subdir = '' ) { + $dest_dir = $this->variables['RUN_DIR'] . "/$subdir"; + + if ( $subdir ) { + mkdir( $dest_dir ); + } + + self::copy_dir( self::$cache_dir, $dest_dir ); + + // disable emailing + mkdir( $dest_dir . '/wp-content/mu-plugins' ); + copy( __DIR__ . '/../extra/no-mail.php', $dest_dir . '/wp-content/mu-plugins/no-mail.php' ); + } + + public function create_config( $subdir = '', $extra_php = false ) { + $params = self::$db_settings; + + // Replaces all characters that are not alphanumeric or an underscore into an underscore. + $params['dbprefix'] = $subdir ? preg_replace( '#[^a-zA-Z\_0-9]#', '_', $subdir ) : 'wp_'; + + $params['skip-salts'] = true; + + if( false !== $extra_php ) { + $params['extra-php'] = $extra_php; + } + + $config_cache_path = ''; + if ( self::$install_cache_dir ) { + $config_cache_path = self::$install_cache_dir . '/config_' . md5( implode( ':', $params ) . ':subdir=' . $subdir ); + $run_dir = '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR']; + } + + if ( $config_cache_path && file_exists( $config_cache_path ) ) { + copy( $config_cache_path, $run_dir . '/wp-config.php' ); + } else { + $this->proc( 'wp config create', $params, $subdir )->run_check(); + if ( $config_cache_path && file_exists( $run_dir . '/wp-config.php' ) ) { + copy( $run_dir . '/wp-config.php', $config_cache_path ); + } + } + } + + public function install_wp( $subdir = '' ) { + $wp_version_suffix = ( $wp_version = getenv( 'WP_VERSION' ) ) ? "-$wp_version" : ''; + 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 ); + } + + $subdir = $this->replace_variables( $subdir ); + + $this->create_db(); + $this->create_run_dir(); + $this->download_wp( $subdir ); + $this->create_config( $subdir ); + + $install_args = array( + 'url' => 'http://example.com', + 'title' => 'WP CLI Site', + 'admin_user' => 'admin', + 'admin_email' => 'admin@example.com', + 'admin_password' => 'password1', + 'skip-email' => true, + ); + + $install_cache_path = ''; + if ( self::$install_cache_dir ) { + $install_cache_path = self::$install_cache_dir . '/install_' . md5( implode( ':', $install_args ) . ':subdir=' . $subdir ); + $run_dir = '' !== $subdir ? ( $this->variables['RUN_DIR'] . "/$subdir" ) : $this->variables['RUN_DIR']; + } + + if ( $install_cache_path && file_exists( $install_cache_path ) ) { + self::copy_dir( $install_cache_path, $run_dir ); + self::run_sql( 'mysql --no-defaults', array( 'execute' => "source {$install_cache_path}.sql" ), true /*add_database*/ ); + } else { + $this->proc( 'wp core install', $install_args, $subdir )->run_check(); + if ( $install_cache_path ) { + mkdir( $install_cache_path ); + self::dir_diff_copy( $run_dir, self::$cache_dir, $install_cache_path ); + self::run_sql( 'mysqldump --no-defaults', array( 'result-file' => "{$install_cache_path}.sql" ), true /*add_database*/ ); + } + } + } + + public function install_wp_with_composer( $vendor_directory = 'vendor' ) { + $this->create_run_dir(); + $this->create_db(); + + $yml_path = $this->variables['RUN_DIR'] . "/wp-cli.yml"; + file_put_contents( $yml_path, 'path: wordpress' ); + + $this->composer_command( 'init --name="wp-cli/composer-test" --type="project" --no-interaction' ); + $this->composer_command( 'config vendor-dir ' . $vendor_directory ); + $this->composer_command( 'require johnpbloch/wordpress --optimize-autoloader --no-interaction' ); + + $config_extra_php = "require_once dirname(__DIR__) . '/" . $vendor_directory . "/autoload.php';"; + $this->create_config( 'wordpress', $config_extra_php ); + + $install_args = array( + 'url' => 'http://localhost:8080', + 'title' => 'WP CLI Site with both WordPress and wp-cli as Composer dependencies', + 'admin_user' => 'admin', + 'admin_email' => 'admin@example.com', + 'admin_password' => 'password1', + 'skip-email' => true, + ); + + $this->proc( 'wp core install', $install_args )->run_check(); + } + + public function composer_add_wp_cli_local_repository() { + if ( ! self::$composer_local_repository ) { + 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 = isset( $env['TRAVIS_BUILD_DIR'] ) ? $env['TRAVIS_BUILD_DIR'] : realpath( __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' ); + } + $dest = self::$composer_local_repository . '/'; + $this->composer_command( "config repositories.wp-cli '{\"type\": \"path\", \"url\": \"$dest\", \"options\": {\"symlink\": false}}'" ); + $this->variables['COMPOSER_LOCAL_REPOSITORY'] = self::$composer_local_repository; + } + + public function composer_require_current_wp_cli() { + $this->composer_add_wp_cli_local_repository(); + $this->composer_command( 'require wp-cli/wp-cli:dev-master --optimize-autoloader --no-interaction' ); + } + + public function start_php_server( $subdir = '' ) { + $dir = $this->variables['RUN_DIR'] . '/'; + if ( $subdir ) { + $dir .= trim( $subdir, '/' ) . '/'; + } + $cmd = Utils\esc_cmd( '%s -S %s -t %s -c %s %s', + Utils\get_php_binary(), + 'localhost:8080', + $dir, + get_cfg_var( 'cfg_file_path' ), + $this->variables['RUN_DIR'] . '/vendor/wp-cli/server-command/router.php' + ); + $this->background_proc( $cmd ); + } + + private function composer_command($cmd) { + if ( !isset( $this->variables['COMPOSER_PATH'] ) ) { + $this->variables['COMPOSER_PATH'] = exec('which composer'); + } + $this->proc( $this->variables['COMPOSER_PATH'] . ' ' . $cmd )->run_check(); + } + + /** + * Initialize run time logging. + */ + private static function log_run_times_before_suite( $event ) { + self::$suite_start_time = microtime( true ); + + Process::$log_run_times = true; + + $travis = getenv( 'TRAVIS' ); + + // Default output settings. + self::$output_to = 'stdout'; + self::$num_top_processes = $travis ? 10 : 40; + self::$num_top_scenarios = $travis ? 10 : 20; + + // Allow setting of above with "WP_CLI_TEST_LOG_RUN_TIMES=[,][,]" formatted env var. + if ( preg_match( '/^(stdout|error_log)?(,[0-9]+)?(,[0-9]+)?$/i', self::$log_run_times, $matches ) ) { + if ( isset( $matches[1] ) ) { + self::$output_to = strtolower( $matches[1] ); + } + if ( isset( $matches[2] ) ) { + self::$num_top_processes = max( (int) substr( $matches[2], 1 ), 1 ); + } + if ( isset( $matches[3] ) ) { + self::$num_top_scenarios = max( (int) substr( $matches[3], 1 ), 1 ); + } + } + } + + /** + * Record the start time of the scenario into the `$scenario_run_times` array. + */ + private static function log_run_times_before_scenario( $event ) { + if ( $scenario_key = self::get_scenario_key( $event ) ) { + self::$scenario_run_times[ $scenario_key ] = -microtime( true ); + } + } + + /** + * Save the run time of the scenario into the `$scenario_run_times` array. Only the top `self::$num_top_scenarios` are kept. + */ + private static function log_run_times_after_scenario( $event ) { + if ( $scenario_key = self::get_scenario_key( $event ) ) { + self::$scenario_run_times[ $scenario_key ] += microtime( true ); + self::$scenario_count++; + if ( count( self::$scenario_run_times ) > self::$num_top_scenarios ) { + arsort( self::$scenario_run_times ); + array_pop( self::$scenario_run_times ); + } + } + } + + /** + * Copy files in updated directory that are not in source directory to copy directory. ("Incremental backup".) + * Note: does not deal with changed files (ie does not compare file contents for changes), for speed reasons. + * + * @param string $upd_dir The directory to search looking for files/directories not in `$src_dir`. + * @param string $src_dir The directory to be compared to `$upd_dir`. + * @param string $cop_dir Where to copy any files/directories in `$upd_dir` but not in `$src_dir` to. + */ + private static function dir_diff_copy( $upd_dir, $src_dir, $cop_dir ) { + if ( false === ( $files = scandir( $upd_dir ) ) ) { + $error = error_get_last(); + throw new \RuntimeException( sprintf( "Failed to open updated directory '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_dir, $error['message'] ) ); + } + foreach ( array_diff( $files, array( '.', '..' ) ) as $file ) { + $upd_file = $upd_dir . '/' . $file; + $src_file = $src_dir . '/' . $file; + $cop_file = $cop_dir . '/' . $file; + if ( ! file_exists( $src_file ) ) { + if ( is_dir( $upd_file ) ) { + if ( ! file_exists( $cop_file ) && ! mkdir( $cop_file, 0777, true /*recursive*/ ) ) { + $error = error_get_last(); + throw new \RuntimeException( sprintf( "Failed to create copy directory '%s': %s. " . __FILE__ . ':' . __LINE__, $cop_file, $error['message'] ) ); + } + self::copy_dir( $upd_file, $cop_file ); + } else { + if ( ! copy( $upd_file, $cop_file ) ) { + $error = error_get_last(); + throw new \RuntimeException( sprintf( "Failed to copy '%s' to '%s': %s. " . __FILE__ . ':' . __LINE__, $upd_file, $cop_file, $error['message'] ) ); + } + } + } elseif ( is_dir( $upd_file ) ) { + self::dir_diff_copy( $upd_file, $src_file, $cop_file ); + } + } + } + + /** + * Get the scenario key used for `$scenario_run_times` array. + * Format " :", eg "core-command core-update.feature:221". + */ + private static function get_scenario_key( $event ) { + $scenario_key = ''; + if ( $file = self::get_event_file( $event, $line ) ) { + $scenario_grandparent = Utils\basename( dirname( dirname( $file ) ) ); + $scenario_key = $scenario_grandparent . ' ' . Utils\basename( $file ) . ':' . $line; + } + return $scenario_key; + } + + /** + * Print out stats on the run times of processes and scenarios. + */ + private static function log_run_times_after_suite( $event ) { + + $suite = ''; + if ( self::$scenario_run_times ) { + // Grandparent directory is first part of key. + $keys = array_keys( self::$scenario_run_times ); + $suite = substr( $keys[0], 0, strpos( $keys[0], ' ' ) ); + } + + $run_from = Utils\basename( dirname( dirname( __DIR__ ) ) ); + + // Format same as Behat, if have minutes. + $fmt = function ( $time ) { + $mins = floor( $time / 60 ); + return round( $time, 3 ) . ( $mins ? ( ' (' . $mins . 'm' . round( $time - ( $mins * 60 ), 3 ) . 's)' ) : '' ); + }; + + $time = microtime( true ) - self::$suite_start_time; + + $log = PHP_EOL . str_repeat( '(', 80 ) . PHP_EOL; + + // Process and proc method run times. + $run_times = array_merge( Process::$run_times, self::$proc_method_run_times ); + + list( $ptime, $calls ) = array_reduce( $run_times, function ( $carry, $item ) { + return array( $carry[0] + $item[0], $carry[1] + $item[1] ); + }, array( 0, 0 ) ); + + $overhead = $time - $ptime; + $pct = round( ( $overhead / $time ) * 100 ); + $unique = count( $run_times ); + + $log .= sprintf( + PHP_EOL . "Total process run time %s (tests %s, overhead %.3f %d%%), calls %d (%d unique) for '%s' run from '%s'" . PHP_EOL, + $fmt( $ptime ), $fmt( $time ), $overhead, $pct, $calls, $unique, $suite, $run_from + ); + + uasort( $run_times, function ( $a, $b ) { + return $a[0] === $b[0] ? 0 : ( $a[0] < $b[0] ? 1 : -1 ); // Reverse sort. + } ); + + $tops = array_slice( $run_times, 0, self::$num_top_processes, true ); + + $log .= PHP_EOL . "Top " . self::$num_top_processes . " process run times for '$suite'"; + $log .= PHP_EOL . implode( PHP_EOL, array_map( function ( $k, $v, $i ) { + return sprintf( ' %3d. %7.3f %3d %s', $i + 1, round( $v[0], 3 ), $v[1], $k ); + }, array_keys( $tops ), $tops, array_keys( array_keys( $tops ) ) ) ) . PHP_EOL; + + // Scenario run times. + arsort( self::$scenario_run_times ); + + $tops = array_slice( self::$scenario_run_times, 0, self::$num_top_scenarios, true ); + + $log .= PHP_EOL . "Top " . self::$num_top_scenarios . " (of " . self::$scenario_count . ") scenario run times for '$suite'"; + $log .= PHP_EOL . implode( PHP_EOL, array_map( function ( $k, $v, $i ) { + return sprintf( ' %3d. %7.3f %s', $i + 1, round( $v, 3 ), substr( $k, strpos( $k, ' ' ) + 1 ) ); + }, array_keys( $tops ), $tops, array_keys( array_keys( $tops ) ) ) ) . PHP_EOL; + + $log .= PHP_EOL . str_repeat( ')', 80 ); + + if ( 'error_log' === self::$output_to ) { + error_log( $log ); + } else { + echo PHP_EOL . $log; + } + } + + /** + * Log the run time of a proc method (one that doesn't use Process but does (use a function that does) a `proc_open()`). + */ + private static function log_proc_method_run_time( $key, $start_time ) { + $run_time = microtime( true ) - $start_time; + if ( ! isset( self::$proc_method_run_times[ $key ] ) ) { + self::$proc_method_run_times[ $key ] = array( 0, 0 ); + } + self::$proc_method_run_times[ $key ][0] += $run_time; + self::$proc_method_run_times[ $key ][1]++; + } + +} diff --git a/features/bootstrap/Process.php b/features/bootstrap/Process.php new file mode 100644 index 000000000..70c3c9f82 --- /dev/null +++ b/features/bootstrap/Process.php @@ -0,0 +1,134 @@ + STDIN, + 1 => array( 'pipe', 'w' ), + 2 => array( '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 = array(); + + /** + * @param string $command Command to execute. + * @param string $cwd Directory to execute the command in. + * @param array $env Environment variables to set when running the command. + * + * @return Process + */ + public static function create( $command, $cwd = null, $env = array() ) { + $proc = new self; + + $proc->command = $command; + $proc->cwd = $cwd; + $proc->env = $env; + + return $proc; + } + + private function __construct() {} + + /** + * Run the command. + * + * @return ProcessRun + */ + public function run() { + $start_time = microtime( true ); + + $proc = proc_open( $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 ] = array( 0, 0 ); + } + self::$run_times[ $this->command ][0] += $run_time; + self::$run_times[ $this->command ][1]++; + } + + return new ProcessRun( + array( + '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 ProcessRun + */ + public function run_check() { + $r = $this->run(); + + // $r->STDERR is incorrect, but kept incorrect for backwards-compat + if ( $r->return_code || ! empty( $r->STDERR ) ) { + 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 ProcessRun + */ + public function run_check_stderr() { + $r = $this->run(); + + if ( $r->return_code || ! empty( $r->stderr ) ) { + throw new \RuntimeException( $r ); + } + + return $r; + } +} diff --git a/features/bootstrap/ProcessRun.php b/features/bootstrap/ProcessRun.php new file mode 100644 index 000000000..96b4c80b6 --- /dev/null +++ b/features/bootstrap/ProcessRun.php @@ -0,0 +1,68 @@ + $value ) { + $this->$key = $value; + } + } + + /** + * Return properties of executed command as a string. + * + * @return string + */ + public function __toString() { + $out = "$ $this->command\n"; + $out .= "$this->stdout\n$this->stderr"; + $out .= "cwd: $this->cwd\n"; + $out .= "run time: $this->run_time\n"; + $out .= "exit status: $this->return_code"; + + return $out; + } + +} diff --git a/features/bootstrap/support.php b/features/bootstrap/support.php new file mode 100644 index 000000000..6aa17c6c3 --- /dev/null +++ b/features/bootstrap/support.php @@ -0,0 +1,200 @@ + $value ) { + if ( ! compareContents( $value, $actual->$name ) ) + return false; + } + } else if ( is_array( $expected ) ) { + foreach ( $expected as $key => $value ) { + if ( ! compareContents( $value, $actual[$key] ) ) + return false; + } + } else { + return $expected === $actual; + } + + return true; +} + +/** + * Compare two strings containing JSON to ensure that @a $actualJson contains at + * least what the JSON string @a $expectedJson contains. + * + * @return whether or not @a $actualJson contains @a $expectedJson + * @retval true @a $actualJson contains @a $expectedJson + * @retval false @a $actualJson does not contain @a $expectedJson + * + * @param[in] $actualJson the JSON string to be tested + * @param[in] $expectedJson the expected JSON string + * + * Examples: + * expected: {'a':1,'array':[1,3,5]} + * + * 1 ) + * actual: {'a':1,'b':2,'c':3,'array':[1,2,3,4,5]} + * return: true + * + * 2 ) + * actual: {'b':2,'c':3,'array':[1,2,3,4,5]} + * return: false + * element 'a' is missing from the root object + * + * 3 ) + * actual: {'a':0,'b':2,'c':3,'array':[1,2,3,4,5]} + * return: false + * the value of element 'a' is not 1 + * + * 4 ) + * actual: {'a':1,'b':2,'c':3,'array':[1,2,4,5]} + * return: false + * the contents of 'array' does not include 3 + */ +function checkThatJsonStringContainsJsonString( $actualJson, $expectedJson ) { + $actualValue = json_decode( $actualJson ); + $expectedValue = json_decode( $expectedJson ); + + if ( !$actualValue ) { + return false; + } + + return compareContents( $expectedValue, $actualValue ); +} + +/** + * Compare two strings to confirm $actualCSV contains $expectedCSV + * Both strings are expected to have headers for their CSVs. + * $actualCSV must match all data rows in $expectedCSV + * + * @param string A CSV string + * @param array A nested array of values + * @return bool Whether $actualCSV contains $expectedCSV + */ +function checkThatCsvStringContainsValues( $actualCSV, $expectedCSV ) { + $actualCSV = array_map( 'str_getcsv', explode( PHP_EOL, $actualCSV ) ); + + if ( empty( $actualCSV ) ) + return false; + + // Each sample must have headers + $actualHeaders = array_values( array_shift( $actualCSV ) ); + $expectedHeaders = array_values( array_shift( $expectedCSV ) ); + + // Each expectedCSV must exist somewhere in actualCSV in the proper column + $expectedResult = 0; + foreach ( $expectedCSV as $expected_row ) { + $expected_row = array_combine( $expectedHeaders, $expected_row ); + foreach ( $actualCSV as $actual_row ) { + + if ( count( $actualHeaders ) != count( $actual_row ) ) + continue; + + $actual_row = array_intersect_key( array_combine( $actualHeaders, $actual_row ), $expected_row ); + if ( $actual_row == $expected_row ) + $expectedResult++; + } + } + + return $expectedResult >= count( $expectedCSV ); +} + +/** + * Compare two strings containing YAML to ensure that @a $actualYaml contains at + * least what the YAML string @a $expectedYaml contains. + * + * @return whether or not @a $actualYaml contains @a $expectedJson + * @retval true @a $actualYaml contains @a $expectedJson + * @retval false @a $actualYaml does not contain @a $expectedJson + * + * @param[in] $actualYaml the YAML string to be tested + * @param[in] $expectedYaml the expected YAML string + */ +function checkThatYamlStringContainsYamlString( $actualYaml, $expectedYaml ) { + $actualValue = Mustangostang\Spyc::YAMLLoad( $actualYaml ); + $expectedValue = Mustangostang\Spyc::YAMLLoad( $expectedYaml ); + + if ( !$actualValue ) { + return false; + } + + return compareContents( $expectedValue, $actualValue ); +} + diff --git a/features/bootstrap/utils.php b/features/bootstrap/utils.php new file mode 100644 index 000000000..bb6e9ad11 --- /dev/null +++ b/features/bootstrap/utils.php @@ -0,0 +1,1320 @@ +config ) && ! empty( $composer->config->{'vendor-dir'} ) ) { + array_unshift( $vendor_paths, WP_CLI_ROOT . '/../../../' . $composer->config->{'vendor-dir'} ); + } + } + return $vendor_paths; +} + +// Using require() directly inside a class grants access to private methods to the loaded code +function load_file( $path ) { + require_once $path; +} + +function load_command( $name ) { + $path = WP_CLI_ROOT . "/php/commands/$name.php"; + + if ( is_readable( $path ) ) { + include_once $path; + } +} + +/** + * Like array_map(), except it returns a new iterator, instead of a modified array. + * + * Example: + * + * $arr = array('Football', 'Socker'); + * + * $it = iterator_map($arr, 'strtolower', function($val) { + * return str_replace('foo', 'bar', $val); + * }); + * + * foreach ( $it as $val ) { + * var_dump($val); + * } + * + * @param array|object Either a plain array or another iterator + * @param callback The function to apply to an element + * @return object An iterator that applies the given callback(s) + */ +function iterator_map( $it, $fn ) { + if ( is_array( $it ) ) { + $it = new \ArrayIterator( $it ); + } + + if ( ! method_exists( $it, 'add_transform' ) ) { + $it = new Transform( $it ); + } + + foreach ( array_slice( func_get_args(), 1 ) as $fn ) { + $it->add_transform( $fn ); + } + + return $it; +} + +/** + * Search for file by walking up the directory tree until the first file is found or until $stop_check($dir) returns true + * @param string|array The files (or file) to search for + * @param string|null The directory to start searching from; defaults to CWD + * @param callable Function which is passed the current dir each time a directory level is traversed + * @return null|string Null if the file was not found + */ +function find_file_upward( $files, $dir = null, $stop_check = null ) { + $files = (array) $files; + if ( is_null( $dir ) ) { + $dir = getcwd(); + } + while ( is_readable( $dir ) ) { + // Stop walking up when the supplied callable returns true being passed the $dir + if ( is_callable( $stop_check ) && call_user_func( $stop_check, $dir ) ) { + return null; + } + + foreach ( $files as $file ) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if ( file_exists( $path ) ) { + return $path; + } + } + + $parent_dir = dirname( $dir ); + if ( empty( $parent_dir ) || $parent_dir === $dir ) { + break; + } + $dir = $parent_dir; + } + return null; +} + +function is_path_absolute( $path ) { + // Windows + if ( isset( $path[1] ) && ':' === $path[1] ) { + return true; + } + + return '/' === $path[0]; +} + +/** + * Composes positional arguments into a command string. + * + * @param array + * @return string + */ +function args_to_str( $args ) { + return ' ' . implode( ' ', array_map( 'escapeshellarg', $args ) ); +} + +/** + * Composes associative arguments into a command string. + * + * @param array + * @return string + */ +function assoc_args_to_str( $assoc_args ) { + $str = ''; + + foreach ( $assoc_args as $key => $value ) { + if ( true === $value ) { + $str .= " --$key"; + } elseif ( is_array( $value ) ) { + foreach ( $value as $_ => $v ) { + $str .= assoc_args_to_str( + array( + $key => $v, + ) + ); + } + } else { + $str .= " --$key=" . escapeshellarg( $value ); + } + } + + return $str; +} + +/** + * Given a template string and an arbitrary number of arguments, + * returns the final command, with the parameters escaped. + */ +function esc_cmd( $cmd ) { + if ( func_num_args() < 2 ) { + trigger_error( 'esc_cmd() requires at least two arguments.', E_USER_WARNING ); + } + + $args = func_get_args(); + + $cmd = array_shift( $args ); + + return vsprintf( $cmd, array_map( 'escapeshellarg', $args ) ); +} + +function locate_wp_config() { + static $path; + + if ( null === $path ) { + $path = false; + + if ( file_exists( ABSPATH . 'wp-config.php' ) ) { + $path = ABSPATH . 'wp-config.php'; + } elseif ( file_exists( ABSPATH . '../wp-config.php' ) && ! file_exists( ABSPATH . '/../wp-settings.php' ) ) { + $path = ABSPATH . '../wp-config.php'; + } + + if ( $path ) { + $path = realpath( $path ); + } + } + + return $path; +} + +function wp_version_compare( $since, $operator ) { + $wp_version = str_replace( '-src', '', $GLOBALS['wp_version'] ); + $since = str_replace( '-src', '', $since ); + return version_compare( $wp_version, $since, $operator ); +} + +/** + * Render a collection of items as an ASCII table, JSON, CSV, YAML, list of ids, or count. + * + * Given a collection of items with a consistent data structure: + * + * ``` + * $items = array( + * array( + * 'key' => 'foo', + * 'value' => 'bar', + * ) + * ); + * ``` + * + * Render `$items` as an ASCII table: + * + * ``` + * WP_CLI\Utils\format_items( 'table', $items, array( 'key', 'value' ) ); + * + * # +-----+-------+ + * # | key | value | + * # +-----+-------+ + * # | foo | bar | + * # +-----+-------+ + * ``` + * + * Or render `$items` as YAML: + * + * ``` + * WP_CLI\Utils\format_items( 'yaml', $items, array( 'key', 'value' ) ); + * + * # --- + * # - + * # key: foo + * # value: bar + * ``` + * + * @access public + * @category Output + * + * @param string $format Format to use: 'table', 'json', 'csv', 'yaml', 'ids', 'count' + * @param array $items An array of items to output. + * @param array|string $fields Named fields for each item of data. Can be array or comma-separated list. + * @return null + */ +function format_items( $format, $items, $fields ) { + $assoc_args = compact( 'format', 'fields' ); + $formatter = new \WP_CLI\Formatter( $assoc_args ); + $formatter->display_items( $items ); +} + +/** + * Write data as CSV to a given file. + * + * @access public + * + * @param resource $fd File descriptor + * @param array $rows Array of rows to output + * @param array $headers List of CSV columns (optional) + */ +function write_csv( $fd, $rows, $headers = array() ) { + if ( ! empty( $headers ) ) { + fputcsv( $fd, $headers ); + } + + foreach ( $rows as $row ) { + if ( ! empty( $headers ) ) { + $row = pick_fields( $row, $headers ); + } + + fputcsv( $fd, array_values( $row ) ); + } +} + +/** + * Pick fields from an associative array or object. + * + * @param array|object Associative array or object to pick fields from + * @param array List of fields to pick + * @return array + */ +function pick_fields( $item, $fields ) { + $item = (object) $item; + + $values = array(); + + foreach ( $fields as $field ) { + $values[ $field ] = isset( $item->$field ) ? $item->$field : null; + } + + return $values; +} + +/** + * Launch system's $EDITOR for the user to edit some text. + * + * @access public + * @category Input + * + * @param string $content Some form of text to edit (e.g. post content) + * @return string|bool Edited text, if file is saved from editor; false, if no change to file. + */ +function launch_editor_for_input( $input, $filename = 'WP-CLI' ) { + + check_proc_available( 'launch_editor_for_input' ); + + $tmpdir = get_temp_dir(); + + do { + $tmpfile = basename( $filename ); + $tmpfile = preg_replace( '|\.[^.]*$|', '', $tmpfile ); + $tmpfile .= '-' . substr( md5( mt_rand() ), 0, 6 ); + $tmpfile = $tmpdir . $tmpfile . '.tmp'; + $fp = fopen( $tmpfile, 'xb' ); + if ( ! $fp && is_writable( $tmpdir ) && file_exists( $tmpfile ) ) { + $tmpfile = ''; + continue; + } + if ( $fp ) { + fclose( $fp ); + } + } while ( ! $tmpfile ); + + if ( ! $tmpfile ) { + \WP_CLI::error( 'Error creating temporary file.' ); + } + + $output = ''; + file_put_contents( $tmpfile, $input ); + + $editor = getenv( 'EDITOR' ); + if ( ! $editor ) { + $editor = 'vi'; + + if ( isset( $_SERVER['OS'] ) && false !== strpos( $_SERVER['OS'], 'indows' ) ) { + $editor = 'notepad'; + } + } + + $descriptorspec = array( STDIN, STDOUT, STDERR ); + $process = proc_open( "$editor " . escapeshellarg( $tmpfile ), $descriptorspec, $pipes ); + $r = proc_close( $process ); + if ( $r ) { + exit( $r ); + } + + $output = file_get_contents( $tmpfile ); + + unlink( $tmpfile ); + + if ( $output === $input ) { + return false; + } + + return $output; +} + +/** + * @param string MySQL host string, as defined in wp-config.php + * @return array + */ +function mysql_host_to_cli_args( $raw_host ) { + $assoc_args = array(); + + $host_parts = explode( ':', $raw_host ); + if ( count( $host_parts ) == 2 ) { + list( $assoc_args['host'], $extra ) = $host_parts; + $extra = trim( $extra ); + if ( is_numeric( $extra ) ) { + $assoc_args['port'] = (int) $extra; + $assoc_args['protocol'] = 'tcp'; + } elseif ( '' !== $extra ) { + $assoc_args['socket'] = $extra; + } + } else { + $assoc_args['host'] = $raw_host; + } + + return $assoc_args; +} + +function run_mysql_command( $cmd, $assoc_args, $descriptors = null ) { + check_proc_available( 'run_mysql_command' ); + + if ( ! $descriptors ) { + $descriptors = array( STDIN, STDOUT, STDERR ); + } + + if ( isset( $assoc_args['host'] ) ) { + //@codingStandardsIgnoreStart + $assoc_args = array_merge( $assoc_args, mysql_host_to_cli_args( $assoc_args['host'] ) ); + //@codingStandardsIgnoreEnd + } + + $pass = $assoc_args['pass']; + unset( $assoc_args['pass'] ); + + $old_pass = getenv( 'MYSQL_PWD' ); + putenv( 'MYSQL_PWD=' . $pass ); + + $final_cmd = force_env_on_nix_systems( $cmd ) . assoc_args_to_str( $assoc_args ); + + $proc = proc_open( $final_cmd, $descriptors, $pipes ); + if ( ! $proc ) { + exit( 1 ); + } + + $r = proc_close( $proc ); + + putenv( 'MYSQL_PWD=' . $old_pass ); + + if ( $r ) { + exit( $r ); + } +} + +/** + * Render PHP or other types of files using Mustache templates. + * + * IMPORTANT: Automatic HTML escaping is disabled! + */ +function mustache_render( $template_name, $data = array() ) { + if ( ! file_exists( $template_name ) ) { + $template_name = WP_CLI_ROOT . "/templates/$template_name"; + } + + $template = file_get_contents( $template_name ); + + $m = new \Mustache_Engine( + array( + 'escape' => function ( $val ) { + return $val; }, + ) + ); + + return $m->render( $template, $data ); +} + +/** + * Create a progress bar to display percent completion of a given operation. + * + * Progress bar is written to STDOUT, and disabled when command is piped. Progress + * advances with `$progress->tick()`, and completes with `$progress->finish()`. + * Process bar also indicates elapsed time and expected total time. + * + * ``` + * # `wp user generate` ticks progress bar each time a new user is created. + * # + * # $ wp user generate --count=500 + * # Generating users 22 % [=======> ] 0:05 / 0:23 + * + * $progress = \WP_CLI\Utils\make_progress_bar( 'Generating users', $count ); + * for ( $i = 0; $i < $count; $i++ ) { + * // uses wp_insert_user() to insert the user + * $progress->tick(); + * } + * $progress->finish(); + * ``` + * + * @access public + * @category Output + * + * @param string $message Text to display before the progress bar. + * @param integer $count Total number of ticks to be performed. + * @return cli\progress\Bar|WP_CLI\NoOp + */ +function make_progress_bar( $message, $count ) { + if ( \cli\Shell::isPiped() ) { + return new \WP_CLI\NoOp; + } + + return new \cli\progress\Bar( $message, $count ); +} + +function parse_url( $url ) { + $url_parts = \parse_url( $url ); + + if ( ! isset( $url_parts['scheme'] ) ) { + $url_parts = parse_url( 'http://' . $url ); + } + + return $url_parts; +} + +/** + * Check if we're running in a Windows environment (cmd.exe). + * + * @return bool + */ +function is_windows() { + return false !== ( $test_is_windows = getenv( 'WP_CLI_TEST_IS_WINDOWS' ) ) ? (bool) $test_is_windows : strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; +} + +/** + * Replace magic constants in some PHP source code. + * + * @param string $source The PHP code to manipulate. + * @param string $path The path to use instead of the magic constants + */ +function replace_path_consts( $source, $path ) { + $replacements = array( + '__FILE__' => "'$path'", + '__DIR__' => "'" . dirname( $path ) . "'", + ); + + $old = array_keys( $replacements ); + $new = array_values( $replacements ); + + return str_replace( $old, $new, $source ); +} + +/** + * Make a HTTP request to a remote URL. + * + * Wraps the Requests HTTP library to ensure every request includes a cert. + * + * ``` + * # `wp core download` verifies the hash for a downloaded WordPress archive + * + * $md5_response = Utils\http_request( 'GET', $download_url . '.md5' ); + * if ( 20 != substr( $md5_response->status_code, 0, 2 ) ) { + * WP_CLI::error( "Couldn't access md5 hash for release (HTTP code {$response->status_code})" ); + * } + * ``` + * + * @access public + * + * @param string $method HTTP method (GET, POST, DELETE, etc.) + * @param string $url URL to make the HTTP request to. + * @param array $headers Add specific headers to the request. + * @param array $options + * @return object + */ +function http_request( $method, $url, $data = null, $headers = array(), $options = array() ) { + + $cert_path = '/rmccue/requests/library/Requests/Transport/cacert.pem'; + $halt_on_error = ! isset( $options['halt_on_error'] ) || (bool) $options['halt_on_error']; + if ( inside_phar() ) { + // cURL can't read Phar archives + $options['verify'] = extract_from_phar( + WP_CLI_VENDOR_DIR . $cert_path + ); + } else { + foreach ( get_vendor_paths() as $vendor_path ) { + if ( file_exists( $vendor_path . $cert_path ) ) { + $options['verify'] = $vendor_path . $cert_path; + break; + } + } + if ( empty( $options['verify'] ) ) { + $error_msg = 'Cannot find SSL certificate.'; + if ( $halt_on_error ) { + WP_CLI::error( $error_msg ); + } + throw new \RuntimeException( $error_msg ); + } + } + + try { + return \Requests::request( $url, $headers, $data, $method, $options ); + } catch ( \Requests_Exception $ex ) { + // CURLE_SSL_CACERT_BADFILE only defined for PHP >= 7. + if ( 'curlerror' !== $ex->getType() || ! in_array( curl_errno( $ex->getData() ), array( CURLE_SSL_CONNECT_ERROR, CURLE_SSL_CERTPROBLEM, 77 /*CURLE_SSL_CACERT_BADFILE*/ ), true ) ) { + $error_msg = sprintf( "Failed to get url '%s': %s.", $url, $ex->getMessage() ); + if ( $halt_on_error ) { + WP_CLI::error( $error_msg ); + } + throw new \RuntimeException( $error_msg, null, $ex ); + } + // Handle SSL certificate issues gracefully + \WP_CLI::warning( sprintf( "Re-trying without verify after failing to get verified url '%s' %s.", $url, $ex->getMessage() ) ); + $options['verify'] = false; + try { + return \Requests::request( $url, $headers, $data, $method, $options ); + } catch ( \Requests_Exception $ex ) { + $error_msg = sprintf( "Failed to get non-verified url '%s' %s.", $url, $ex->getMessage() ); + if ( $halt_on_error ) { + WP_CLI::error( $error_msg ); + } + throw new \RuntimeException( $error_msg, null, $ex ); + } + } +} + +/** + * Increments a version string using the "x.y.z-pre" format + * + * Can increment the major, minor or patch number by one + * If $new_version == "same" the version string is not changed + * If $new_version is not a known keyword, it will be used as the new version string directly + * + * @param string $current_version + * @param string $new_version + * @return string + */ +function increment_version( $current_version, $new_version ) { + // split version assuming the format is x.y.z-pre + $current_version = explode( '-', $current_version, 2 ); + $current_version[0] = explode( '.', $current_version[0] ); + + switch ( $new_version ) { + case 'same': + // do nothing + break; + + case 'patch': + $current_version[0][2]++; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + case 'minor': + $current_version[0][1]++; + $current_version[0][2] = 0; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + case 'major': + $current_version[0][0]++; + $current_version[0][1] = 0; + $current_version[0][2] = 0; + + $current_version = array( $current_version[0] ); // drop possible pre-release info + break; + + default: // not a keyword + $current_version = array( array( $new_version ) ); + break; + } + + // reconstruct version string + $current_version[0] = implode( '.', $current_version[0] ); + $current_version = implode( '-', $current_version ); + + return $current_version; +} + +/** + * Compare two version strings to get the named semantic version. + * + * @access public + * + * @param string $new_version + * @param string $original_version + * @return string $name 'major', 'minor', 'patch' + */ +function get_named_sem_ver( $new_version, $original_version ) { + + if ( ! Comparator::greaterThan( $new_version, $original_version ) ) { + return ''; + } + + $parts = explode( '-', $original_version ); + $bits = explode( '.', $parts[0] ); + $major = $bits[0]; + if ( isset( $bits[1] ) ) { + $minor = $bits[1]; + } + if ( isset( $bits[2] ) ) { + $patch = $bits[2]; + } + + if ( ! is_null( $minor ) && Semver::satisfies( $new_version, "{$major}.{$minor}.x" ) ) { + return 'patch'; + } + + if ( Semver::satisfies( $new_version, "{$major}.x.x" ) ) { + return 'minor'; + } + + return 'major'; +} + +/** + * Return the flag value or, if it's not set, the $default value. + * + * Because flags can be negated (e.g. --no-quiet to negate --quiet), this + * function provides a safer alternative to using + * `isset( $assoc_args['quiet'] )` or similar. + * + * @access public + * @category Input + * + * @param array $assoc_args Arguments array. + * @param string $flag Flag to get the value. + * @param mixed $default Default value for the flag. Default: NULL + * @return mixed + */ +function get_flag_value( $assoc_args, $flag, $default = null ) { + return isset( $assoc_args[ $flag ] ) ? $assoc_args[ $flag ] : $default; +} + +/** + * Get the home directory. + * + * @access public + * @category System + * + * @return string + */ +function get_home_dir() { + $home = getenv( 'HOME' ); + if ( ! $home ) { + // In Windows $HOME may not be defined + $home = getenv( 'HOMEDRIVE' ) . getenv( 'HOMEPATH' ); + } + + return rtrim( $home, '/\\' ); +} + +/** + * Appends a trailing slash. + * + * @access public + * @category System + * + * @param string $string What to add the trailing slash to. + * @return string String with trailing slash added. + */ +function trailingslashit( $string ) { + return rtrim( $string, '/\\' ) . '/'; +} + +/** + * Get the system's temp directory. Warns user if it isn't writable. + * + * @access public + * @category System + * + * @return string + */ +function get_temp_dir() { + static $temp = ''; + + if ( $temp ) { + return $temp; + } + + $temp = '/tmp/'; + + // `sys_get_temp_dir()` introduced PHP 5.2.1. + if ( $try = sys_get_temp_dir() ) { + $temp = trailingslashit( $try ); + } elseif ( $try = ini_get( 'upload_tmp_dir' ) ) { + $temp = trailingslashit( $try ); + } + + if ( ! is_writable( $temp ) ) { + \WP_CLI::warning( "Temp directory isn't writable: {$temp}" ); + } + + return $temp; +} + +/** + * Parse a SSH url for its host, port, and path. + * + * Similar to parse_url(), but adds support for defined SSH aliases. + * + * ``` + * host OR host/path/to/wordpress OR host:port/path/to/wordpress + * ``` + * + * @access public + * + * @return mixed + */ +function parse_ssh_url( $url, $component = -1 ) { + preg_match( '#^((docker|docker\-compose|ssh|vagrant):)?(([^@:]+)@)?([^:/~]+)(:([\d]*))?((/|~)(.+))?$#', $url, $matches ); + $bits = array(); + foreach ( array( + 2 => 'scheme', + 4 => 'user', + 5 => 'host', + 7 => 'port', + 8 => 'path', + ) as $i => $key ) { + if ( ! empty( $matches[ $i ] ) ) { + $bits[ $key ] = $matches[ $i ]; + } + } + + // Find the hostname from `vagrant ssh-config` automatically. + if ( preg_match( '/^vagrant:?/', $url ) ) { + if ( 'vagrant' === $bits['host'] && empty( $bits['scheme'] ) ) { + $ssh_config = shell_exec( 'vagrant ssh-config 2>/dev/null' ); + if ( preg_match( '/Host\s(.+)/', $ssh_config, $matches ) ) { + $bits['scheme'] = 'vagrant'; + $bits['host'] = $matches[1]; + } + } + } + + switch ( $component ) { + case PHP_URL_SCHEME: + return isset( $bits['scheme'] ) ? $bits['scheme'] : null; + case PHP_URL_USER: + return isset( $bits['user'] ) ? $bits['user'] : null; + case PHP_URL_HOST: + return isset( $bits['host'] ) ? $bits['host'] : null; + case PHP_URL_PATH: + return isset( $bits['path'] ) ? $bits['path'] : null; + case PHP_URL_PORT: + return isset( $bits['port'] ) ? $bits['port'] : null; + default: + return $bits; + } +} + +/** + * Report the results of the same operation against multiple resources. + * + * @access public + * @category Input + * + * @param string $noun Resource being affected (e.g. plugin) + * @param string $verb Type of action happening to the noun (e.g. activate) + * @param integer $total Total number of resource being affected. + * @param integer $successes Number of successful operations. + * @param integer $failures Number of failures. + * @param null|integer $skips Optional. Number of skipped operations. Default null (don't show skips). + */ +function report_batch_operation_results( $noun, $verb, $total, $successes, $failures, $skips = null ) { + $plural_noun = $noun . 's'; + $past_tense_verb = past_tense_verb( $verb ); + $past_tense_verb_upper = ucfirst( $past_tense_verb ); + if ( $failures ) { + $failed_skipped_message = null === $skips ? '' : " ({$failures} failed" . ( $skips ? ", {$skips} skipped" : '' ) . ')'; + if ( $successes ) { + WP_CLI::error( "Only {$past_tense_verb} {$successes} of {$total} {$plural_noun}{$failed_skipped_message}." ); + } else { + WP_CLI::error( "No {$plural_noun} {$past_tense_verb}{$failed_skipped_message}." ); + } + } else { + $skipped_message = $skips ? " ({$skips} skipped)" : ''; + if ( $successes || $skips ) { + WP_CLI::success( "{$past_tense_verb_upper} {$successes} of {$total} {$plural_noun}{$skipped_message}." ); + } else { + $message = $total > 1 ? ucfirst( $plural_noun ) : ucfirst( $noun ); + WP_CLI::success( "{$message} already {$past_tense_verb}." ); + } + } +} + +/** + * Parse a string of command line arguments into an $argv-esqe variable. + * + * @access public + * @category Input + * + * @param string $arguments + * @return array + */ +function parse_str_to_argv( $arguments ) { + preg_match_all( '/(?<=^|\s)([\'"]?)(.+?)(? 'check-update', + 'clear' => 'flush', + 'decrement' => 'decr', + 'del' => 'delete', + 'directory' => 'dir', + 'exec' => 'eval', + 'exec-file' => 'eval-file', + 'increment' => 'incr', + 'language' => 'locale', + 'lang' => 'locale', + 'new' => 'create', + 'number' => 'count', + 'remove' => 'delete', + 'regen' => 'regenerate', + 'rep' => 'replace', + 'repl' => 'replace', + 'v' => 'version', + ); + + if ( array_key_exists( $target, $suggestion_map ) ) { + return $suggestion_map[ $target ]; + } + + if ( empty( $options ) ) { + return ''; + } + foreach ( $options as $option ) { + $distance = levenshtein( $option, $target ); + $levenshtein[ $option ] = $distance; + } + + // Sort known command strings by distance to user entry. + asort( $levenshtein ); + + // Fetch the closest command string. + reset( $levenshtein ); + $suggestion = key( $levenshtein ); + + // Only return a suggestion if below a given threshold. + return $levenshtein[ $suggestion ] <= $threshold && $suggestion !== $target + ? (string) $suggestion + : ''; +} + +/** + * Get a Phar-safe version of a path. + * + * For paths inside a Phar, this strips the outer filesystem's location to + * reduce the path to what it needs to be within the Phar archive. + * + * Use the __FILE__ or __DIR__ constants as a starting point. + * + * @param string $path An absolute path that might be within a Phar. + * + * @return string A Phar-safe version of the path. + */ +function phar_safe_path( $path ) { + + if ( ! inside_phar() ) { + return $path; + } + + return str_replace( + PHAR_STREAM_PREFIX . WP_CLI_PHAR_PATH . '/', + PHAR_STREAM_PREFIX, + $path + ); +} + +/** + * Check whether a given Command object is part of the bundled set of + * commands. + * + * This function accepts both a fully qualified class name as a string as + * well as an object that extends `WP_CLI\Dispatcher\CompositeCommand`. + * + * @param \WP_CLI\Dispatcher\CompositeCommand|string $command + * + * @return bool + */ +function is_bundled_command( $command ) { + static $classes; + + if ( null === $classes ) { + $classes = array(); + $class_map = WP_CLI_VENDOR_DIR . '/composer/autoload_commands_classmap.php'; + if ( file_exists( WP_CLI_VENDOR_DIR . '/composer/' ) ) { + $classes = include $class_map; + } + } + + if ( is_object( $command ) ) { + $command = get_class( $command ); + } + + return is_string( $command ) + ? array_key_exists( $command, $classes ) + : false; +} + +/** + * Maybe prefix command string with "/usr/bin/env". + * Removes (if there) if Windows, adds (if not there) if not. + * + * @param string $command + * + * @return string + */ +function force_env_on_nix_systems( $command ) { + $env_prefix = '/usr/bin/env '; + $env_prefix_len = strlen( $env_prefix ); + if ( is_windows() ) { + if ( 0 === strncmp( $command, $env_prefix, $env_prefix_len ) ) { + $command = substr( $command, $env_prefix_len ); + } + } else { + if ( 0 !== strncmp( $command, $env_prefix, $env_prefix_len ) ) { + $command = $env_prefix . $command; + } + } + return $command; +} + +/** + * Check that `proc_open()` and `proc_close()` haven't been disabled. + * + * @param string $context Optional. If set will appear in error message. Default null. + * @param bool $return Optional. If set will return false rather than error out. Default false. + * + * @return bool + */ +function check_proc_available( $context = null, $return = false ) { + if ( ! function_exists( 'proc_open' ) || ! function_exists( 'proc_close' ) ) { + if ( $return ) { + return false; + } + $msg = 'The PHP functions `proc_open()` and/or `proc_close()` are disabled. Please check your PHP ini directive `disable_functions` or suhosin settings.'; + if ( $context ) { + WP_CLI::error( sprintf( "Cannot do '%s': %s", $context, $msg ) ); + } else { + WP_CLI::error( $msg ); + } + } + return true; +} + +/** + * Returns past tense of verb, with limited accuracy. Only regular verbs catered for, apart from "reset". + * + * @param string $verb Verb to return past tense of. + * + * @return string + */ +function past_tense_verb( $verb ) { + static $irregular = array( + 'reset' => 'reset', + ); + if ( isset( $irregular[ $verb ] ) ) { + return $irregular[ $verb ]; + } + $last = substr( $verb, -1 ); + if ( 'e' === $last ) { + $verb = substr( $verb, 0, -1 ); + } elseif ( 'y' === $last && ! preg_match( '/[aeiou]y$/', $verb ) ) { + $verb = substr( $verb, 0, -1 ) . 'i'; + } elseif ( preg_match( '/^[^aeiou]*[aeiou][^aeiouhwxy]$/', $verb ) ) { + // Rule of thumb that most (all?) one-voweled regular verbs ending in vowel + consonant (excluding "h", "w", "x", "y") double their final consonant - misses many cases (eg "submit"). + $verb .= $last; + } + return $verb . 'ed'; +} + +/** + * Get the path to the PHP binary used when executing WP-CLI. + * + * Environment values permit specific binaries to be indicated. + * + * @access public + * @category System + * + * @return string + */ +function get_php_binary() { + if ( $wp_cli_php_used = getenv( 'WP_CLI_PHP_USED' ) ) { + return $wp_cli_php_used; + } + + if ( $wp_cli_php = getenv( 'WP_CLI_PHP' ) ) { + return $wp_cli_php; + } + + // Available since PHP 5.4. + if ( defined( 'PHP_BINARY' ) ) { + return PHP_BINARY; + } + + // @codingStandardsIgnoreLine + if ( @is_executable( PHP_BINDIR . '/php' ) ) { + return PHP_BINDIR . '/php'; + } + + // @codingStandardsIgnoreLine + if ( is_windows() && @is_executable( PHP_BINDIR . '/php.exe' ) ) { + return PHP_BINDIR . '/php.exe'; + } + + return 'php'; +} diff --git a/features/cache-patch.feature b/features/cache-patch.feature deleted file mode 100644 index fe027adba..000000000 --- a/features/cache-patch.feature +++ /dev/null @@ -1,126 +0,0 @@ -Feature: Patch command available for the object cache - - Scenario: Nested values from cache can be updated at any depth. - Given a WP install - And a wp-content/mu-plugins/test-harness.php file: - """php - 'bar'] ); - wp_cache_set( 'other_key', ['fuz' => 'biz'] ); - wp_cache_set( 'my_key_in_group', ['fuz' => 'biz'], 'my_group' ); - - $complex_key = (object) [ - 'foo' => (object) [ - 'bar' => (object) [ - 'baz' => 2, - ], - ], - ]; - wp_cache_set( 'complex_key', $complex_key ); - }; - - WP_CLI::add_hook( 'before_invoke:cache patch', $set_foo ); - """ - - When I run `wp cache patch insert my_key fuz baz` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key'. - """ - - When I run `wp cache patch insert complex_key foo bar fuz 34` - Then STDOUT should be: - """ - Success: Updated cache key 'complex_key'. - """ - - When I try `wp cache patch insert my_key_in_group foo bar --group=my_group` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key_in_group'. - """ - - When I try `wp cache patch insert unknown_key foo bar` - Then STDERR should be: - """ - Error: Cannot create key "foo" on data type boolean - """ - - When I try `wp cache patch insert other_key foo bar --group=unknown_group` - Then STDERR should be: - """ - Error: Cannot create key "foo" on data type boolean - """ - - When I run `wp cache patch update my_key foo test` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key'. - """ - - When I run `wp cache patch update other_key fuz biz` - Then STDOUT should be: - """ - Success: Value passed for cache key 'other_key' is unchanged. - """ - - When I run `wp cache patch update complex_key foo bar baz 34` - Then STDOUT should be: - """ - Success: Updated cache key 'complex_key'. - """ - - When I try `wp cache patch update my_key_in_group fuz baz --group=my_group` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key_in_group'. - """ - - When I try `wp cache patch update unknown_key foo test` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ - - When I try `wp cache patch update my_key bar test` - Then STDERR should be: - """ - Error: No data exists for key "bar" - """ - - When I try `wp cache patch update my_key foo baz --expiration=60` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key'. - """ - - When I run `wp cache patch delete my_key foo` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key'. - """ - - When I try `wp cache patch delete my_key_in_group fuz --group=my_group` - Then STDOUT should be: - """ - Success: Updated cache key 'my_key_in_group'. - """ - - When I try `wp cache patch delete unknown_key foo` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ - - When I try `wp cache patch delete my_key bar` - Then STDERR should be: - """ - Error: No data exists for key "bar" - """ - - When I try `wp cache patch delete my_key foo --group=my_group` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ diff --git a/features/cache-pluck.feature b/features/cache-pluck.feature deleted file mode 100644 index c930b084b..000000000 --- a/features/cache-pluck.feature +++ /dev/null @@ -1,52 +0,0 @@ -Feature: Pluck command available for the object cache - - Scenario: Nested values from cache can be retrieved at any depth. - Given a WP install - And a wp-content/mu-plugins/test-harness.php file: - """php - 'bar'] ); - wp_cache_set( 'my_key_2', ['foo' => ['bar' => 'baz']] ); - wp_cache_set( 'my_key_3', ['foo' => 'bar_custom'], 'my_custom_group' ); - }; - - WP_CLI::add_hook( 'before_invoke:cache pluck', $set_foo ); - """ - - When I run `wp cache pluck my_key foo` - Then STDOUT should be: - """ - bar - """ - - When I run `wp cache pluck my_key_2 foo bar` - Then STDOUT should be: - """ - baz - """ - - When I run `wp cache pluck my_key_2 foo bar --format=json` - Then STDOUT should be: - """ - "baz" - """ - - When I run `wp cache pluck my_key_2 foo --format=json` - Then STDOUT should be: - """ - {"bar":"baz"} - """ - - When I run `wp cache pluck my_key_3 foo --group=my_custom_group` - Then STDOUT should be: - """ - bar_custom - """ - - When I try `wp cache pluck unknown_key test` - Then STDERR should be: - """ - Warning: No object found for the key 'unknown_key' in group 'default' - """ - diff --git a/features/cache.feature b/features/cache.feature index 21c23a663..472f28d19 100644 --- a/features/cache.feature +++ b/features/cache.feature @@ -1,6 +1,5 @@ Feature: Managed the WordPress object cache - @skip-object-cache Scenario: Default group is 'default' Given a WP install And a wp-content/mu-plugins/test-harness.php file: @@ -133,92 +132,3 @@ Feature: Managed the WordPress object cache """ Error: Could not replace object 'bar' in group 'foo'. Does it not exist? """ - - @require-wp-6.1 - Scenario: Some cache groups cannot be cleared. - Given a WP install - When I run `wp cache flush-group add_multiple` - Then STDOUT should be: - """ - Success: Cache group 'add_multiple' was flushed. - """ - - @require-wp-6.1 - Scenario: Some cache groups cannot be cleared. - Given a WP install - And a wp-content/mu-plugins/unclearable-test-cache.php file: - """php - Given( '/^an empty directory$/', + function ( $world ) { + $world->create_run_dir(); + } +); + +$steps->Given( '/^an? (empty|non-existent) ([^\s]+) directory$/', + function ( $world, $empty_or_nonexistent, $dir ) { + $dir = $world->replace_variables( $dir ); + if ( ! WP_CLI\Utils\is_path_absolute( $dir ) ) { + $dir = $world->variables['RUN_DIR'] . "/$dir"; + } + if ( 0 !== strpos( $dir, sys_get_temp_dir() ) ) { + throw new RuntimeException( sprintf( "Attempted to delete directory '%s' that is not in the temp directory '%s'. " . __FILE__ . ':' . __LINE__, $dir, sys_get_temp_dir() ) ); + } + $world->remove_dir( $dir ); + if ( 'empty' === $empty_or_nonexistent ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } + } +); + +$steps->Given( '/^an empty cache/', + function ( $world ) { + $world->variables['SUITE_CACHE_DIR'] = FeatureContext::create_cache_dir(); + } +); + +$steps->Given( '/^an? ([^\s]+) file:$/', + function ( $world, $path, PyStringNode $content ) { + $content = (string) $content . "\n"; + $full_path = $world->variables['RUN_DIR'] . "/$path"; + $dir = dirname( $full_path ); + if ( ! file_exists( $dir ) ) { + mkdir( $dir, 0777, true /*recursive*/ ); + } + file_put_contents( $full_path, $content ); + } +); + +$steps->Given( '/^"([^"]+)" replaced with "([^"]+)" in the ([^\s]+) file$/', function( $world, $search, $replace, $path ) { + $full_path = $world->variables['RUN_DIR'] . "/$path"; + $contents = file_get_contents( $full_path ); + $contents = str_replace( $search, $replace, $contents ); + file_put_contents( $full_path, $contents ); +}); + +$steps->Given( '/^WP files$/', + function ( $world ) { + $world->download_wp(); + } +); + +$steps->Given( '/^wp-config\.php$/', + function ( $world ) { + $world->create_config(); + } +); + +$steps->Given( '/^a database$/', + function ( $world ) { + $world->create_db(); + } +); + +$steps->Given( '/^a WP (install|installation)$/', + function ( $world ) { + $world->install_wp(); + } +); + +$steps->Given( "/^a WP (install|installation) in '([^\s]+)'$/", + function ( $world, $_, $subdir ) { + $world->install_wp( $subdir ); + } +); + +$steps->Given( '/^a WP (install|installation) with Composer$/', + function ( $world ) { + $world->install_wp_with_composer(); + } +); + +$steps->Given( "/^a WP (install|installation) with Composer and a custom vendor directory '([^\s]+)'$/", + function ( $world, $_, $vendor_directory ) { + $world->install_wp_with_composer( $vendor_directory ); + } +); + +$steps->Given( '/^a WP multisite (subdirectory|subdomain)?\s?(install|installation)$/', + function ( $world, $type = 'subdirectory' ) { + $world->install_wp(); + $subdomains = ! empty( $type ) && 'subdomain' === $type ? 1 : 0; + $world->proc( 'wp core install-network', array( 'title' => 'WP CLI Network', 'subdomains' => $subdomains ) )->run_check(); + } +); + +$steps->Given( '/^these installed and active plugins:$/', + function( $world, $stream ) { + $plugins = implode( ' ', array_map( 'trim', explode( PHP_EOL, (string)$stream ) ) ); + $world->proc( "wp plugin install $plugins --activate" )->run_check(); + } +); + +$steps->Given( '/^a custom wp-content directory$/', + function ( $world ) { + $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; + + $wp_config_code = file_get_contents( $wp_config_path ); + + $world->move_files( 'wp-content', 'my-content' ); + $world->add_line_to_wp_config( $wp_config_code, + "define( 'WP_CONTENT_DIR', dirname(__FILE__) . '/my-content' );" ); + + $world->move_files( 'my-content/plugins', 'my-plugins' ); + $world->add_line_to_wp_config( $wp_config_code, + "define( 'WP_PLUGIN_DIR', __DIR__ . '/my-plugins' );" ); + + file_put_contents( $wp_config_path, $wp_config_code ); + } +); + +$steps->Given( '/^download:$/', + function ( $world, TableNode $table ) { + foreach ( $table->getHash() as $row ) { + $path = $world->replace_variables( $row['path'] ); + if ( file_exists( $path ) ) { + // assume it's the same file and skip re-download + continue; + } + + Process::create( \WP_CLI\Utils\esc_cmd( 'curl -sSL %s > %s', $row['url'], $path ) )->run_check(); + } + } +); + +$steps->Given( '/^save (STDOUT|STDERR) ([\'].+[^\'])?\s?as \{(\w+)\}$/', + function ( $world, $stream, $output_filter, $key ) { + + $stream = strtolower( $stream ); + + if ( $output_filter ) { + $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; + if ( false !== preg_match( $output_filter, $world->result->$stream, $matches ) ) + $output = array_pop( $matches ); + else + $output = ''; + } else { + $output = $world->result->$stream; + } + $world->variables[ $key ] = trim( $output, "\n" ); + } +); + +$steps->Given( '/^a new Phar with (?:the same version|version "([^"]+)")$/', + function ( $world, $version = 'same' ) { + $world->build_phar( $version ); + } +); + +$steps->Given( '/^a downloaded Phar with (?:the same version|version "([^"]+)")$/', + function ( $world, $version = 'same' ) { + $world->download_phar( $version ); + } +); + +$steps->Given( '/^save the (.+) file ([\'].+[^\'])?as \{(\w+)\}$/', + function ( $world, $filepath, $output_filter, $key ) { + $full_file = file_get_contents( $world->replace_variables( $filepath ) ); + + if ( $output_filter ) { + $output_filter = '/' . trim( str_replace( '%s', '(.+[^\b])', $output_filter ), "' " ) . '/'; + if ( false !== preg_match( $output_filter, $full_file, $matches ) ) + $output = array_pop( $matches ); + else + $output = ''; + } else { + $output = $full_file; + } + $world->variables[ $key ] = trim( $output, "\n" ); + } +); + +$steps->Given('/^a misconfigured WP_CONTENT_DIR constant directory$/', + function($world) { + $wp_config_path = $world->variables['RUN_DIR'] . "/wp-config.php"; + + $wp_config_code = file_get_contents( $wp_config_path ); + + $world->add_line_to_wp_config( $wp_config_code, + "define( 'WP_CONTENT_DIR', '' );" ); + + file_put_contents( $wp_config_path, $wp_config_code ); + } +); + +$steps->Given( '/^a dependency on current wp-cli$/', + function ( $world ) { + $world->composer_require_current_wp_cli(); + } +); + +$steps->Given( '/^a PHP built-in web server$/', + function ( $world ) { + $world->start_php_server(); + } +); + +$steps->Given( "/^a PHP built-in web server to serve '([^\s]+)'$/", + function ( $world, $subdir ) { + $world->start_php_server( $subdir ); + } +); diff --git a/features/steps/then.php b/features/steps/then.php new file mode 100644 index 000000000..21589e737 --- /dev/null +++ b/features/steps/then.php @@ -0,0 +1,237 @@ +Then( '/^the return code should( not)? be (\d+)$/', + function ( $world, $not, $return_code ) { + if ( ( ! $not && $return_code != $world->result->return_code ) || ( $not && $return_code == $world->result->return_code ) ) { + throw new RuntimeException( $world->result ); + } + } +); + +$steps->Then( '/^(STDOUT|STDERR) should (be|contain|not contain):$/', + function ( $world, $stream, $action, PyStringNode $expected ) { + + $stream = strtolower( $stream ); + + $expected = $world->replace_variables( (string) $expected ); + + checkString( $world->result->$stream, $expected, $action, $world->result ); + } +); + +$steps->Then( '/^(STDOUT|STDERR) should be a number$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + assertNumeric( trim( $world->result->$stream, "\n" ) ); + } +); + +$steps->Then( '/^(STDOUT|STDERR) should not be a number$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + assertNotNumeric( trim( $world->result->$stream, "\n" ) ); + } +); + +$steps->Then( '/^STDOUT should be a table containing rows:$/', + function ( $world, TableNode $expected ) { + $output = $world->result->stdout; + $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); + + $expected_rows = array(); + foreach ( $expected->getRows() as $row ) { + $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); + } + + compareTables( $expected_rows, $actual_rows, $output ); + } +); + +$steps->Then( '/^STDOUT should end with a table containing rows:$/', + function ( $world, TableNode $expected ) { + $output = $world->result->stdout; + $actual_rows = explode( "\n", rtrim( $output, "\n" ) ); + + $expected_rows = array(); + foreach ( $expected->getRows() as $row ) { + $expected_rows[] = $world->replace_variables( implode( "\t", $row ) ); + } + + $start = array_search( $expected_rows[0], $actual_rows ); + + if ( false === $start ) + throw new \Exception( $world->result ); + + compareTables( $expected_rows, array_slice( $actual_rows, $start ), $output ); + } +); + +$steps->Then( '/^STDOUT should be JSON containing:$/', + function ( $world, PyStringNode $expected ) { + $output = $world->result->stdout; + $expected = $world->replace_variables( (string) $expected ); + + if ( !checkThatJsonStringContainsJsonString( $output, $expected ) ) { + throw new \Exception( $world->result ); + } +}); + +$steps->Then( '/^STDOUT should be a JSON array containing:$/', + function ( $world, PyStringNode $expected ) { + $output = $world->result->stdout; + $expected = $world->replace_variables( (string) $expected ); + + $actualValues = json_decode( $output ); + $expectedValues = json_decode( $expected ); + + $missing = array_diff( $expectedValues, $actualValues ); + if ( !empty( $missing ) ) { + throw new \Exception( $world->result ); + } +}); + +$steps->Then( '/^STDOUT should be CSV containing:$/', + function ( $world, TableNode $expected ) { + $output = $world->result->stdout; + + $expected_rows = $expected->getRows(); + foreach ( $expected as &$row ) { + foreach ( $row as &$value ) { + $value = $world->replace_variables( $value ); + } + } + + if ( ! checkThatCsvStringContainsValues( $output, $expected_rows ) ) + throw new \Exception( $world->result ); + } +); + +$steps->Then( '/^STDOUT should be YAML containing:$/', + function ( $world, PyStringNode $expected ) { + $output = $world->result->stdout; + $expected = $world->replace_variables( (string) $expected ); + + if ( !checkThatYamlStringContainsYamlString( $output, $expected ) ) { + throw new \Exception( $world->result ); + } +}); + +$steps->Then( '/^(STDOUT|STDERR) should be empty$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + if ( !empty( $world->result->$stream ) ) { + throw new \Exception( $world->result ); + } + } +); + +$steps->Then( '/^(STDOUT|STDERR) should not be empty$/', + function ( $world, $stream ) { + + $stream = strtolower( $stream ); + + if ( '' === rtrim( $world->result->$stream, "\n" ) ) { + throw new Exception( $world->result ); + } + } +); + +$steps->Then( '/^(STDOUT|STDERR) should be a version string (<|<=|>|>=|==|=|!=|<>) ([+\w.{}-]+)$/', + function ( $world, $stream, $operator, $goal_ver ) { + $goal_ver = $world->replace_variables( $goal_ver ); + $stream = strtolower( $stream ); + if ( false === version_compare( trim( $world->result->$stream, "\n" ), $goal_ver, $operator ) ) { + throw new Exception( $world->result ); + } + } +); + +$steps->Then( '/^the (.+) (file|directory) should (exist|not exist|be:|contain:|not contain:)$/', + function ( $world, $path, $type, $action, $expected = null ) { + $path = $world->replace_variables( $path ); + + // If it's a relative path, make it relative to the current test dir + if ( '/' !== $path[0] ) + $path = $world->variables['RUN_DIR'] . "/$path"; + + if ( 'file' == $type ) { + $test = 'file_exists'; + } else if ( 'directory' == $type ) { + $test = 'is_dir'; + } + + switch ( $action ) { + case 'exist': + if ( ! $test( $path ) ) { + throw new Exception( "$path doesn't exist." ); + } + break; + case 'not exist': + if ( $test( $path ) ) { + throw new Exception( "$path exists." ); + } + break; + default: + if ( ! $test( $path ) ) { + throw new Exception( "$path doesn't exist." ); + } + $action = substr( $action, 0, -1 ); + $expected = $world->replace_variables( (string) $expected ); + if ( 'file' == $type ) { + $contents = file_get_contents( $path ); + } else if ( 'directory' == $type ) { + $files = glob( rtrim( $path, '/' ) . '/*' ); + foreach( $files as &$file ) { + $file = str_replace( $path . '/', '', $file ); + } + $contents = implode( PHP_EOL, $files ); + } + checkString( $contents, $expected, $action ); + } + } +); + +$steps->Then( '/^the contents of the (.+) file should match (((\/.+\/)|(#.+#))([a-z]+)?)$/', + function ( $world, $path, $expected ) { + $path = $world->replace_variables( $path ); + // If it's a relative path, make it relative to the current test dir + if ( '/' !== $path[0] ) { + $path = $world->variables['RUN_DIR'] . "/$path"; + } + $contents = file_get_contents( $path ); + assertRegExp( $expected, $contents ); + } +); + +$steps->Then( '/^(STDOUT|STDERR) should match (((\/.+\/)|(#.+#))([a-z]+)?)$/', + function ( $world, $stream, $expected ) { + $stream = strtolower( $stream ); + assertRegExp( $expected, $world->result->$stream ); + } +); + +$steps->Then( '/^an email should (be sent|not be sent)$/', function( $world, $expected ) { + if ( 'be sent' === $expected ) { + assertNotEquals( 0, $world->email_sends ); + } else if ( 'not be sent' === $expected ) { + assertEquals( 0, $world->email_sends ); + } else { + throw new Exception( 'Invalid expectation' ); + } +}); + +$steps->Then( '/^the HTTP status code should be (\d+)$/', + function ( $world, $return_code ) { + $response = \Requests::request( 'http://localhost:8080' ); + assertEquals( $return_code, $response->status_code ); + } +); diff --git a/features/steps/when.php b/features/steps/when.php new file mode 100644 index 000000000..d23aa0e66 --- /dev/null +++ b/features/steps/when.php @@ -0,0 +1,54 @@ + 'run_check_stderr', + 'try' => 'run' + ); + $method = $map[ $mode ]; + + return $proc->$method(); +} + +function capture_email_sends( $stdout ) { + $stdout = preg_replace( '#WP-CLI test suite: Sent email to.+\n?#', '', $stdout, -1, $email_sends ); + return array( $stdout, $email_sends ); +} + +$steps->When( '/^I launch in the background `([^`]+)`$/', + function ( $world, $cmd ) { + $world->background_proc( $cmd ); + } +); + +$steps->When( '/^I (run|try) `([^`]+)`$/', + function ( $world, $mode, $cmd ) { + $cmd = $world->replace_variables( $cmd ); + $world->result = invoke_proc( $world->proc( $cmd ), $mode ); + list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); + } +); + +$steps->When( "/^I (run|try) `([^`]+)` from '([^\s]+)'$/", + function ( $world, $mode, $cmd, $subdir ) { + $cmd = $world->replace_variables( $cmd ); + $world->result = invoke_proc( $world->proc( $cmd, array(), $subdir ), $mode ); + list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); + } +); + +$steps->When( '/^I (run|try) the previous command again$/', + function ( $world, $mode ) { + if ( !isset( $world->result ) ) + throw new \Exception( 'No previous command.' ); + + $proc = Process::create( $world->result->command, $world->result->cwd, $world->result->env ); + $world->result = invoke_proc( $proc, $mode ); + list( $world->result->stdout, $world->email_sends ) = capture_email_sends( $world->result->stdout ); + } +); + diff --git a/features/transient-patch.feature b/features/transient-patch.feature deleted file mode 100644 index f588cd9d2..000000000 --- a/features/transient-patch.feature +++ /dev/null @@ -1,343 +0,0 @@ -Feature: Patch command available for the transient cache - - Scenario: Nested values from transient can be inserted at any depth. - Given a WP install - And I run `wp eval "set_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient patch insert my_key fuz baz` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json` - Then STDOUT should be: - """ - {"foo":"bar","fuz":"baz"} - """ - - When I run `wp transient patch insert my_key foo bar` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key' is unchanged. - """ - - When I run `wp transient get my_key --format=json` - Then STDOUT should be: - """ - {"foo":"bar","fuz":"baz"} - """ - - When I run `wp transient patch insert my_key_2 foo fuz biz` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json` - Then STDOUT should be: - """ - {"foo":{"bar":"baz","fuz":"biz"}} - """ - - When I run `wp transient patch insert my_key_2 foo bar baz` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key_2' is unchanged. - """ - - When I run `wp transient get my_key_2 --format=json` - Then STDOUT should be: - """ - {"foo":{"bar":"baz","fuz":"biz"}} - """ - - When I run `wp transient patch insert my_key fiz bar --expiration=300` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json` - Then STDOUT should be: - """ - {"foo":"bar","fuz":"baz","fiz":"bar"} - """ - - When I try `wp transient patch insert unknown_key foo bar` - Then STDERR should be: - """ - Error: Cannot create key "foo" on data type boolean - """ - - Scenario: Nested values from transient can be updated at any depth. - Given a WP install - And I run `wp eval "set_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient patch update my_key foo baz` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json` - Then STDOUT should be: - """ - {"foo":"baz"} - """ - - When I run `wp transient patch update my_key foo baz` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key' is unchanged. - """ - - When I run `wp transient get my_key --format=json` - Then STDOUT should be: - """ - {"foo":"baz"} - """ - - When I run `wp transient patch update my_key_2 foo bar biz` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json` - Then STDOUT should be: - """ - {"foo":{"bar":"biz"}} - """ - - When I run `wp transient patch update my_key_2 foo bar biz` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key_2' is unchanged. - """ - - When I run `wp transient get my_key_2 --format=json` - Then STDOUT should be: - """ - {"foo":{"bar":"biz"}} - """ - - When I try `wp transient patch update unknown_key foo bar` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ - - Scenario: Nested values from transient can be deleted at any depth. - Given a WP install - And I run `wp eval "set_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient patch delete my_key foo` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json` - Then STDOUT should be: - """ - [] - """ - - When I run `wp transient patch delete my_key_2 foo bar` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json` - Then STDOUT should be: - """ - {"foo":[]} - """ - - When I run `wp transient patch delete my_key_2 foo` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json` - Then STDOUT should be: - """ - [] - """ - - When I try `wp transient patch delete unknown_key foo` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ - - Scenario: Nested values from site transient can be inserted at any depth. - Given a WP multisite install - And I run `wp eval "set_site_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_site_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient patch insert my_key fuz baz --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json --network` - Then STDOUT should be: - """ - {"foo":"bar","fuz":"baz"} - """ - - When I run `wp transient patch insert my_key foo bar --network` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key' is unchanged. - """ - - When I run `wp transient get my_key --format=json --network` - Then STDOUT should be: - """ - {"foo":"bar","fuz":"baz"} - """ - - When I run `wp transient patch insert my_key_2 foo fuz biz --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json --network` - Then STDOUT should be: - """ - {"foo":{"bar":"baz","fuz":"biz"}} - """ - - When I run `wp transient patch insert my_key_2 foo bar baz --network` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key_2' is unchanged. - """ - - When I run `wp transient get my_key_2 --format=json --network` - Then STDOUT should be: - """ - {"foo":{"bar":"baz","fuz":"biz"}} - """ - - When I try `wp transient patch insert unknown_key foo bar --network` - Then STDERR should be: - """ - Error: Cannot create key "foo" on data type boolean - """ - - Scenario: Nested values from site transient can be updated at any depth. - Given a WP multisite install - And I run `wp eval "set_site_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_site_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient patch update my_key foo baz --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json --network` - Then STDOUT should be: - """ - {"foo":"baz"} - """ - - When I run `wp transient patch update my_key foo baz --network` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key' is unchanged. - """ - - When I run `wp transient get my_key --format=json --network` - Then STDOUT should be: - """ - {"foo":"baz"} - """ - - When I run `wp transient patch update my_key_2 foo bar biz --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json --network` - Then STDOUT should be: - """ - {"foo":{"bar":"biz"}} - """ - - When I run `wp transient patch update my_key_2 foo bar biz --network` - Then STDOUT should be: - """ - Success: Value passed for transient 'my_key_2' is unchanged. - """ - - When I run `wp transient get my_key_2 --format=json --network` - Then STDOUT should be: - """ - {"foo":{"bar":"biz"}} - """ - - When I try `wp transient patch update unknown_key foo bar --network` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ - - Scenario: Nested values from site transient can be deleted at any depth. - Given a WP multisite install - And I run `wp eval "set_site_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_site_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient patch delete my_key foo --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key'. - """ - - When I run `wp transient get my_key --format=json --network` - Then STDOUT should be: - """ - [] - """ - - When I run `wp transient patch delete my_key_2 foo bar --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json --network` - Then STDOUT should be: - """ - {"foo":[]} - """ - - When I run `wp transient patch delete my_key_2 foo --network` - Then STDOUT should be: - """ - Success: Updated transient 'my_key_2'. - """ - - When I run `wp transient get my_key_2 --format=json --network` - Then STDOUT should be: - """ - [] - """ - - When I try `wp transient patch delete unknown_key foo --network` - Then STDERR should be: - """ - Error: No data exists for key "foo" - """ diff --git a/features/transient-pluck.feature b/features/transient-pluck.feature deleted file mode 100644 index baaef81dc..000000000 --- a/features/transient-pluck.feature +++ /dev/null @@ -1,59 +0,0 @@ -Feature: Pluck command available for the transient cache - - Scenario: Nested values from transient can be retrieved at any depth. - Given a WP install - And I run `wp eval "set_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient pluck my_key foo` - Then STDOUT should be: - """ - bar - """ - - When I run `wp transient pluck my_key_2 foo bar` - Then STDOUT should be: - """ - baz - """ - - When I run `wp transient pluck my_key_2 foo bar --format=json` - Then STDOUT should be: - """ - "baz" - """ - - When I run `wp transient pluck my_key_2 foo --format=json` - Then STDOUT should be: - """ - {"bar":"baz"} - """ - - When I try `wp transient pluck unknown_key foo` - Then STDERR should be: - """ - Warning: Transient with key "unknown_key" is not set. - """ - - Scenario: Nested values from site transient can be retrieved at any depth. - Given a WP multisite install - And I run `wp eval "set_site_transient( 'my_key', ['foo' => 'bar'] );"` - And I run `wp eval "set_site_transient( 'my_key_2', ['foo' => ['bar' => 'baz']] );"` - - When I run `wp transient pluck my_key foo --network` - Then STDOUT should be: - """ - bar - """ - - When I run `wp transient pluck my_key_2 foo bar --network` - Then STDOUT should be: - """ - baz - """ - - When I try `wp transient pluck unknown_key foo --network` - Then STDERR should be: - """ - Warning: Transient with key "unknown_key" is not set. - """ diff --git a/features/transient.feature b/features/transient.feature index 10111b0ad..064d4d883 100644 --- a/features/transient.feature +++ b/features/transient.feature @@ -55,160 +55,8 @@ Feature: Manage WordPress transient cache Success: Transient deleted. """ - @skip-object-cache - Scenario: Deleting all transients on single site + Scenario: Transient delete and other flags Given a WP install - # We set `WP_DEVELOPMENT_MODE` to stop WordPress from automatically creating - # additional transients which cause some steps to fail when testing. - And I run `wp config set WP_DEVELOPMENT_MODE all` - - And I run `wp transient list --format=count` - And save STDOUT as {EXISTING_TRANSIENTS} - And I run `expr {EXISTING_TRANSIENTS} + 2` - And save STDOUT as {EXPECTED_TRANSIENTS} - - When I try `wp transient delete` - Then STDERR should be: - """ - Error: Please specify transient key, or use --all or --expired. - """ - - When I run `wp transient set foo bar` - And I run `wp transient set foo2 bar2 600` - And I run `wp transient set foo3 bar3 --network` - And I run `wp transient set foo4 bar4 600 --network` - - And I run `wp transient delete --all` - Then STDOUT should be: - """ - Success: {EXPECTED_TRANSIENTS} transients deleted from the database. - """ - - When I try `wp transient get foo` - Then STDERR should be: - """ - Warning: Transient with key "foo" is not set. - """ - - When I try `wp transient get foo2` - Then STDERR should be: - """ - Warning: Transient with key "foo2" is not set. - """ - - When I run `wp transient get foo3 --network` - Then STDOUT should be: - """ - bar3 - """ - - When I run `wp transient get foo4 --network` - Then STDOUT should be: - """ - bar4 - """ - - When I run `wp transient delete --all --network` - Then STDOUT should be: - """ - Success: 2 transients deleted from the database. - """ - - When I try `wp transient get foo3 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo3" is not set. - """ - - When I try `wp transient get foo4 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo4" is not set. - """ - - @skip-object-cache - Scenario: Deleting expired transients on single site - Given a WP install - And I run `wp transient set foo bar 600` - And I run `wp transient set foo2 bar2 600` - And I run `wp transient set foo3 bar3 600 --network` - And I run `wp transient set foo4 bar4 600 --network` - # Change timeout to be in the past. - And I run `wp option update _transient_timeout_foo 1321009871` - And I run `wp option update _site_transient_timeout_foo3 1321009871` - - When I run `wp transient delete --expired` - Then STDOUT should be: - """ - Success: 1 expired transient deleted from the database. - """ - - When I try `wp transient get foo` - Then STDERR should be: - """ - Warning: Transient with key "foo" is not set. - """ - - When I run `wp transient get foo2` - Then STDOUT should be: - """ - bar2 - """ - - # Check if option still exists as a get transient call will remove it. - When I run `wp option get _site_transient_foo3` - Then STDOUT should be: - """ - bar3 - """ - - When I run `wp transient get foo4 --network` - Then STDOUT should be: - """ - bar4 - """ - - When I run `wp transient delete --expired --network` - Then STDOUT should be: - """ - Success: 1 expired transient deleted from the database. - """ - - When I try `wp transient get foo` - Then STDERR should be: - """ - Warning: Transient with key "foo" is not set. - """ - - When I run `wp transient get foo2` - Then STDOUT should be: - """ - bar2 - """ - - When I try `wp transient get foo3 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo3" is not set. - """ - - When I run `wp transient get foo4 --network` - Then STDOUT should be: - """ - bar4 - """ - - @skip-object-cache - Scenario: Deleting all transients on multisite - Given a WP multisite install - # We set `WP_DEVELOPMENT_MODE` to stop WordPress from automatically creating - # additional transients which cause some steps to fail when testing. - And I run `wp config set WP_DEVELOPMENT_MODE all` - And I run `wp site create --slug=foo` - And I run `wp transient list --format=count` - And save STDOUT as {EXISTING_TRANSIENTS} - And I run `expr {EXISTING_TRANSIENTS} + 2` - And save STDOUT as {EXPECTED_TRANSIENTS} When I try `wp transient delete` Then STDERR should be: @@ -217,15 +65,11 @@ Feature: Manage WordPress transient cache """ When I run `wp transient set foo bar` - And I run `wp transient set foo2 bar2 600` - And I run `wp transient set foo3 bar3 --network` - And I run `wp transient set foo4 bar4 600 --network` - And I run `wp --url=example.com/foo transient set foo5 bar5 --network` - And I run `wp --url=example.com/foo transient set foo6 bar6 600 --network` + And I run `wp transient set foo2 bar2` And I run `wp transient delete --all` - Then STDOUT should be: + Then STDOUT should contain: """ - Success: {EXPECTED_TRANSIENTS} transients deleted from the database. + transients deleted from the database. """ When I try `wp transient get foo` @@ -239,411 +83,3 @@ Feature: Manage WordPress transient cache """ Warning: Transient with key "foo2" is not set. """ - - When I run `wp transient get foo3 --network` - Then STDOUT should be: - """ - bar3 - """ - - When I run `wp transient get foo4 --network` - Then STDOUT should be: - """ - bar4 - """ - - When I run `wp --url=example.com/foo transient get foo5 --network` - Then STDOUT should be: - """ - bar5 - """ - - When I run `wp --url=example.com/foo transient get foo6 --network` - Then STDOUT should be: - """ - bar6 - """ - - When I run `wp transient delete --all --network` - Then STDOUT should be: - """ - Success: 4 transients deleted from the database. - """ - - When I try `wp transient get foo3 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo3" is not set. - """ - - When I try `wp transient get foo4 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo4" is not set. - """ - - When I try `wp --url=example.com/foo transient get foo5 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo5" is not set. - """ - - When I try `wp --url=example.com/foo transient get foo6 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo6" is not set. - """ - - @skip-object-cache - Scenario: Deleting expired transients on multisite - Given a WP multisite install - And I run `wp site create --slug=foo` - And I run `wp transient set foo bar 600` - And I run `wp transient set foo2 bar2 600` - And I run `wp transient set foo3 bar3 600 --network` - And I run `wp transient set foo4 bar4 600 --network` - And I run `wp --url=example.com/foo transient set foo5 bar5 600 --network` - And I run `wp --url=example.com/foo transient set foo6 bar6 600 --network` - # Change timeout to be in the past. - And I run `wp option update _transient_timeout_foo 1321009871` - And I run `wp site option update _site_transient_timeout_foo3 1321009871` - And I run `wp --url=example.com/foo site option update _site_transient_timeout_foo5 1321009871` - - When I run `wp transient delete --expired` - Then STDOUT should be: - """ - Success: 1 expired transient deleted from the database. - """ - - When I try `wp transient get foo` - Then STDERR should be: - """ - Warning: Transient with key "foo" is not set. - """ - - When I run `wp transient get foo2` - Then STDOUT should be: - """ - bar2 - """ - - # Check if option still exists as a get transient call will remove it. - When I run `wp site option get _site_transient_foo3` - Then STDOUT should be: - """ - bar3 - """ - - When I run `wp transient get foo4 --network` - Then STDOUT should be: - """ - bar4 - """ - - # Check if option still exists as a get transient call will remove it. - When I run `wp --url=example.com/foo site option get _site_transient_foo5` - Then STDOUT should be: - """ - bar5 - """ - - When I run `wp --url=example.com/foo transient get foo6 --network` - Then STDOUT should be: - """ - bar6 - """ - - When I run `wp transient delete --expired --network` - Then STDOUT should be: - """ - Success: 2 expired transients deleted from the database. - """ - - When I try `wp transient get foo` - Then STDERR should be: - """ - Warning: Transient with key "foo" is not set. - """ - - When I run `wp transient get foo2` - Then STDOUT should be: - """ - bar2 - """ - - When I try `wp transient get foo3 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo3" is not set. - """ - - When I run `wp transient get foo4 --network` - Then STDOUT should be: - """ - bar4 - """ - - When I try `wp --url=example.com/foo transient get foo5 --network` - Then STDERR should be: - """ - Warning: Transient with key "foo5" is not set. - """ - - When I run `wp --url=example.com/foo transient get foo6 --network` - Then STDOUT should be: - """ - bar6 - """ - - @skip-object-cache - Scenario: List transients on single site - Given a WP install - And I run `wp transient set foo bar` - And I run `wp transient set foo2 bar2 610` - And I run `wp option update _transient_timeout_foo2 95649119999` - And I run `wp transient set foo3 bar3 300` - And I run `wp option update _transient_timeout_foo3 1321009871` - And I run `wp transient set foo4 bar4 --network` - And I run `wp transient set foo5 bar5 610 --network` - And I run `wp option update _site_transient_timeout_foo5 95649119999` - And I run `wp transient set foo6 bar6 300 --network` - And I run `wp option update _site_transient_timeout_foo6 1321009871` - - When I run `wp transient list --format=csv` - Then STDOUT should contain: - """ - foo,bar,false - """ - And STDOUT should contain: - """ - foo2,bar2,95649119999 - """ - And STDOUT should contain: - """ - foo3,bar3,1321009871 - """ - - When I run `wp transient list --format=csv --human-readable` - Then STDOUT should contain: - """ - foo,bar,"never expires" - """ - And STDOUT should contain: - """ - foo3,bar3,expired - """ - And STDOUT should not contain: - """ - foo2,bar2,95649119999 - """ - - When I run `wp transient list --network --format=csv` - Then STDOUT should contain: - """ - foo4,bar4,false - """ - And STDOUT should contain: - """ - foo5,bar5,95649119999 - """ - And STDOUT should contain: - """ - foo6,bar6,1321009871 - """ - - @skip-object-cache - Scenario: List transients on multisite - Given a WP multisite install - # We set `WP_DEVELOPMENT_MODE` to stop WordPress from automatically creating - # additional transients which cause some steps to fail when testing. - And I run `wp config set WP_DEVELOPMENT_MODE all` - And I run `wp transient set foo bar` - And I run `wp transient set foo2 bar2 610` - And I run `wp option update _transient_timeout_foo2 95649119999` - And I run `wp transient set foo3 bar3 300` - And I run `wp option update _transient_timeout_foo3 1321009871` - And I run `wp transient set foo4 bar4 --network` - And I run `wp transient set foo5 bar5 610 --network` - And I run `wp site option update _site_transient_timeout_foo5 95649119999` - And I run `wp transient set foo6 bar6 300 --network` - And I run `wp site option update _site_transient_timeout_foo6 1321009871` - - When I run `wp transient list --format=csv` - Then STDOUT should contain: - """ - foo,bar,false - """ - And STDOUT should contain: - """ - foo2,bar2,95649119999 - """ - And STDOUT should contain: - """ - foo3,bar3,1321009871 - """ - - When I run `wp transient list --format=csv --human-readable` - Then STDOUT should contain: - """ - foo,bar,"never expires" - """ - And STDOUT should contain: - """ - foo3,bar3,expired - """ - And STDOUT should not contain: - """ - foo2,bar2,95649119999 - """ - - When I run `wp transient list --network --format=csv` - Then STDOUT should contain: - """ - foo4,bar4,false - """ - And STDOUT should contain: - """ - foo5,bar5,95649119999 - """ - And STDOUT should contain: - """ - foo6,bar6,1321009871 - """ - - @skip-object-cache - Scenario: List transients with search and exclude pattern - Given a WP install - And I run `wp transient set foo bar` - And I run `wp transient set foo2 bar2` - And I run `wp transient set foo3 bar3` - And I run `wp transient set foo4 bar4 --network` - And I run `wp transient set foo5 bar5 --network` - - When I run `wp transient list --format=csv --fields=name --search="foo"` - Then STDOUT should be: - """ - name - foo - """ - - When I run `wp transient list --format=csv --fields=name --search="foo*"` - Then STDOUT should be: - """ - name - foo - foo2 - foo3 - """ - - When I run `wp transient list --format=csv --fields=name --search="*oo"` - Then STDOUT should be: - """ - name - foo - """ - - When I run `wp transient list --format=csv --fields=name --search="*oo*"` - Then STDOUT should be: - """ - name - foo - foo2 - foo3 - """ - - When I run `wp transient list --format=csv --fields=name --search="*oo?"` - Then STDOUT should be: - """ - name - foo2 - foo3 - """ - - When I run `wp transient list --format=csv --fields=name --search="foo?"` - Then STDOUT should be: - """ - name - foo2 - foo3 - """ - - When I run `wp transient list --format=csv --fields=name --search="doesnotexist*"` - Then STDOUT should be: - """ - name - """ - - When I run `wp transient list --format=csv --fields=name --search="foo*" --exclude="foo2"` - Then STDOUT should be: - """ - name - foo - foo3 - """ - - When I run `wp transient list --format=csv --fields=name --search="foo*" --exclude="*3"` - Then STDOUT should be: - """ - name - foo - foo2 - """ - - When I run `wp transient list --format=csv --fields=name --search="foo*" --exclude="foo?"` - Then STDOUT should be: - """ - name - foo - """ - - When I run `wp transient list --format=csv --fields=name --search="foo*" --network` - Then STDOUT should be: - """ - name - foo4 - foo5 - """ - - When I run `wp transient list --format=csv --fields=name --search="foo*" --exclude="foo5" --network` - Then STDOUT should be: - """ - name - foo4 - """ - - @require-object-cache - Scenario: Transient database operations warn when external object cache is active - Given a WP install - - When I try `wp transient list --format=count` - Then STDERR should be: - """ - Warning: Transients are stored in an external object cache, and this command only shows those stored in the database. - """ - And STDOUT should be: - """ - 0 - """ - And the return code should be 0 - - When I try `wp transient delete --all` - Then STDERR should be: - """ - Warning: Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients. - """ - And STDOUT should be: - """ - Success: No transients found. - """ - And the return code should be 0 - - When I try `wp transient delete --expired` - Then STDERR should be: - """ - Warning: Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients. - """ - And STDOUT should be: - """ - Success: No expired transients found. - """ - And the return code should be 0 diff --git a/phpcs.xml.dist b/phpcs.xml.dist deleted file mode 100644 index 4546b0918..000000000 --- a/phpcs.xml.dist +++ /dev/null @@ -1,66 +0,0 @@ - - - Custom ruleset for WP-CLI cache-command - - - - - . - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */src/(Cache|Transient)_Command\.php$ - - - diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 18a583110..000000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,19 +0,0 @@ -parameters: - level: 9 - paths: - - src - - cache-command.php - scanDirectories: - - vendor/wp-cli/wp-cli/php - scanFiles: - - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php - treatPhpDocTypesAsCertain: false - strictRules: - uselessCast: true - closureUsesThis: true - overwriteVariablesWithLoop: true - matchingInheritedMethodNames: true - numericOperandsInArithmeticOperators: true - switchConditionsMatchingType: true - ignoreErrors: - - identifier: missingType.return diff --git a/src/Cache_Command.php b/src/Cache_Command.php index ab5c8b56d..cca4ec3ae 100644 --- a/src/Cache_Command.php +++ b/src/Cache_Command.php @@ -1,8 +1,5 @@ $assoc_args Associative arguments. + * $ wp cache add my_key my_group my_value 300 + * Success: Added object 'my_key' in group 'my_value'. */ public function add( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; - if ( ! wp_cache_add( $key, $value, $group, (int) $expiration ) ) { + if ( ! wp_cache_add( $key, $value, $group, $expiration ) ) { WP_CLI::error( "Could not add object '$key' in group '$group'. Does it already exist?" ); } @@ -99,13 +93,10 @@ public function add( $args, $assoc_args ) { * # Decrease cache value. * $ wp cache decr my_key 2 my_group * 48 - * - * @param array{string, string, string} $args Positional arguments. - * @param array $assoc_args Associative arguments. */ public function decr( $args, $assoc_args ) { list( $key, $offset, $group ) = $args; - $value = wp_cache_decr( $key, (int) $offset, $group ); + $value = wp_cache_decr( $key, $offset, $group ); if ( false === $value ) { WP_CLI::error( 'The value was not decremented.' ); @@ -135,13 +126,10 @@ public function decr( $args, $assoc_args ) { * # Delete cache. * $ wp cache delete my_key my_group * Success: Object deleted. - * - * @param array{string, string} $args Positional arguments. - * @param array $assoc_args Associative arguments. */ public function delete( $args, $assoc_args ) { list( $key, $group ) = $args; - $result = wp_cache_delete( $key, $group ); + $result = wp_cache_delete( $key, $group ); if ( false === $result ) { WP_CLI::error( 'The object was not deleted.' ); @@ -166,13 +154,9 @@ public function delete( $args, $assoc_args ) { * $ wp cache flush * Success: The cache was flushed. */ - public function flush() { - // TODO: Needs fixing in wp-cli/wp-cli - if ( WP_CLI::has_config( 'url' ) && ! empty( WP_CLI::get_config()['url'] ) && is_multisite() ) { - WP_CLI::warning( 'Flushing the cache may affect all sites in a multisite installation, depending on the implementation of the object cache.' ); - } - + public function flush( $args, $assoc_args ) { $value = wp_cache_flush(); + if ( false === $value ) { WP_CLI::error( 'The object cache could not be flushed.' ); } @@ -201,13 +185,10 @@ public function flush() { * # Get cache. * $ wp cache get my_key my_group * my_value - * - * @param array{string, string} $args Positional arguments. - * @param array $assoc_args Associative arguments. */ public function get( $args, $assoc_args ) { list( $key, $group ) = $args; - $value = wp_cache_get( $key, $group ); + $value = wp_cache_get( $key, $group ); if ( false === $value ) { WP_CLI::error( "Object with key '$key' and group '$group' not found." ); @@ -243,13 +224,10 @@ public function get( $args, $assoc_args ) { * # Increase cache value. * $ wp cache incr my_key 2 my_group * 50 - * - * @param array{string, string, string} $args Positional arguments. - * @param array $assoc_args Associative arguments. */ public function incr( $args, $assoc_args ) { list( $key, $offset, $group ) = $args; - $value = wp_cache_incr( $key, (int) $offset, $group ); + $value = wp_cache_incr( $key, $offset, $group ); if ( false === $value ) { WP_CLI::error( 'The value was not incremented.' ); @@ -288,13 +266,10 @@ public function incr( $args, $assoc_args ) { * # Replace cache. * $ wp cache replace my_key new_value my_group * Success: Replaced object 'my_key' in group 'my_group'. - * - * @param array{string, string, string, string} $args Positional arguments. - * @param array $assoc_args Associative arguments. */ public function replace( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; - $result = wp_cache_replace( $key, $value, $group, (int) $expiration ); + $result = wp_cache_replace( $key, $value, $group, $expiration ); if ( false === $result ) { WP_CLI::error( "Could not replace object '$key' in group '$group'. Does it not exist?" ); @@ -333,13 +308,10 @@ public function replace( $args, $assoc_args ) { * # Set cache. * $ wp cache set my_key my_value my_group 300 * Success: Set object 'my_key' in group 'my_group'. - * - * @param array{string, string, string, string} $args Positional arguments. - * @param array $assoc_args Associative arguments. */ public function set( $args, $assoc_args ) { list( $key, $value, $group, $expiration ) = $args; - $result = wp_cache_set( $key, $value, $group, (int) $expiration ); + $result = wp_cache_set( $key, $value, $group, $expiration ); if ( false === $result ) { WP_CLI::error( "Could not add object '$key' in group '$group'." ); @@ -362,253 +334,9 @@ public function set( $args, $assoc_args ) { * $ wp cache type * Default */ - public function type() { + public function type( $args, $assoc_args ) { $message = WP_CLI\Utils\wp_get_cache_type(); WP_CLI::line( $message ); } - /** - * Determines whether the object cache implementation supports a particular feature. - * - * ## OPTIONS - * - * - * : Name of the feature to check for. - * - * ## EXAMPLES - * - * # Check whether is add_multiple supported. - * $ wp cache supports add_multiple - * $ echo $? - * 0 - * - * # Bash script for checking whether for support like this: - * if ! wp cache supports non_existing; then - * echo 'non_existing is not supported' - * fi - * - * @param array{string} $args Positional arguments. - */ - public function supports( $args ) { - list ( $feature ) = $args; - - if ( ! function_exists( 'wp_cache_supports' ) ) { - WP_CLI::error( 'Checking cache features is only available in WordPress 6.1 and higher' ); - } - - $supports = wp_cache_supports( $feature ); - - if ( $supports ) { - WP_CLI::halt( 0 ); - } - WP_CLI::halt( 1 ); - } - - /** - * Removes all cache items in a group, if the object cache implementation supports it. - * - * ## OPTIONS - * - * - * : Cache group key. - * - * ## EXAMPLES - * - * # Clear cache group. - * $ wp cache flush-group my_group - * Success: Cache group 'my_group' was flushed. - * - * @subcommand flush-group - * - * @param array{string} $args Positional arguments. - */ - public function flush_group( $args ) { - list( $group ) = $args; - - if ( ! function_exists( 'wp_cache_supports' ) || ! wp_cache_supports( 'flush_group' ) ) { - WP_CLI::error( 'Group flushing is not supported.' ); - } - - if ( ! wp_cache_flush_group( $group ) ) { - WP_CLI::error( "Cache group '$group' was not flushed." ); - } - WP_CLI::success( "Cache group '$group' was flushed." ); - } - - /** - * Get a nested value from the cache. - * - * ## OPTIONS - * - * - * : Cache key. - * - * ... - * : The name(s) of the keys within the value to locate the value to pluck. - * - * [--group=] - * : Method for grouping data within the cache which allows the same key to be used across groups. - * --- - * default: default - * --- - * - * [--format=] - * : The output format of the value. - * --- - * default: plaintext - * options: - * - plaintext - * - json - * - yaml - * --- - * - * @param array{string, string} $args Positional arguments. - * @param array{group: string, format: string} $assoc_args Associative arguments. - */ - public function pluck( $args, $assoc_args ) { - list( $key ) = $args; - - $group = Utils\get_flag_value( $assoc_args, 'group' ); - - $value = wp_cache_get( $key, $group ); - - if ( false === $value ) { - WP_CLI::warning( "No object found for the key '$key' in group '$group'" ); - exit; - } - - $key_path = array_map( - function ( $key ) { - if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { - return (int) $key; - } - return $key; - }, - array_slice( $args, 1 ) - ); - - $traverser = new RecursiveDataStructureTraverser( $value ); - - try { - $value = $traverser->get( $key_path ); - } catch ( \Exception $e ) { - die( 1 ); - } - - WP_CLI::print_value( $value, $assoc_args ); - } - - /** - * Update a nested value from the cache. - * - * ## OPTIONS - * - * - * : Patch action to perform. - * --- - * options: - * - insert - * - update - * - delete - * --- - * - * - * : Cache key. - * - * ... - * : The name(s) of the keys within the value to locate the value to patch. - * - * [] - * : The new value. If omitted, the value is read from STDIN. - * - * [--group=] - * : Method for grouping data within the cache which allows the same key to be used across groups. - * --- - * default: default - * --- - * - * [--expiration=] - * : Define how long to keep the value, in seconds. `0` means as long as possible. - * --- - * default: 0 - * --- - * - * [--format=] - * : The serialization format for the value. - * --- - * default: plaintext - * options: - * - plaintext - * - json - * --- - * - * @param string[] $args Positional arguments. - * @param array{group: string, expiration: string, format: string} $assoc_args Associative arguments. - */ - public function patch( $args, $assoc_args ) { - list( $action, $key ) = $args; - - $group = Utils\get_flag_value( $assoc_args, 'group' ); - - $expiration = (int) Utils\get_flag_value( $assoc_args, 'expiration' ); - - $key_path = array_map( - function ( $key ) { - if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { - return (int) $key; - } - - return $key; - }, - array_slice( $args, 2 ) - ); - - if ( 'delete' === $action ) { - $patch_value = null; - } else { - $stdin_value = Utils\has_stdin() - ? trim( WP_CLI::get_value_from_arg_or_stdin( $args, -1 ) ) - : null; - - if ( ! empty( $stdin_value ) ) { - $patch_value = WP_CLI::read_value( $stdin_value, $assoc_args ); - } elseif ( count( $key_path ) > 1 ) { - $patch_value = WP_CLI::read_value( (string) array_pop( $key_path ), $assoc_args ); - } else { - $patch_value = null; - } - - if ( null === $patch_value ) { - WP_CLI::error( 'Please provide value to update.' ); - } - } - - /* Need to make a copy of $current_value here as it is modified by reference */ - $old_value = wp_cache_get( $key, $group ); - $current_value = $old_value; - if ( is_object( $old_value ) ) { - $current_value = clone $old_value; - } - - $traverser = new RecursiveDataStructureTraverser( $current_value ); - - try { - $traverser->$action( $key_path, $patch_value ); - } catch ( \Exception $e ) { - WP_CLI::error( $e->getMessage() ); - } - - $patched_value = $traverser->value(); - - if ( $patched_value === $old_value ) { - WP_CLI::success( "Value passed for cache key '$key' is unchanged." ); - } else { - $success = wp_cache_set( $key, $patched_value, $group, $expiration ); - if ( $success ) { - WP_CLI::success( "Updated cache key '$key'." ); - } else { - WP_CLI::error( "Could not update cache key '$key'." ); - } - } - } } diff --git a/src/Transient_Command.php b/src/Transient_Command.php index e29b5dc16..b6f644c82 100644 --- a/src/Transient_Command.php +++ b/src/Transient_Command.php @@ -1,8 +1,5 @@ delete_all( $network ); + $this->delete_all(); return; - } elseif ( true === $expired ) { - $this->delete_expired( $network ); + } + else if ( true === $expired ) { + $this->delete_expired(); return; } @@ -209,17 +182,16 @@ public function delete( $args, $assoc_args ) { WP_CLI::error( 'Please specify transient key, or use --all or --expired.' ); } - $func = $network ? 'delete_site_transient' : 'delete_transient'; + $func = \WP_CLI\Utils\get_flag_value( $assoc_args, 'network' ) ? 'delete_site_transient' : 'delete_transient'; if ( $func( $key ) ) { WP_CLI::success( 'Transient deleted.' ); } else { - $func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; - if ( $func( $key ) ) { + $func = \WP_CLI\Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; + if ( $func( $key ) ) WP_CLI::error( 'Transient was not deleted even though the transient appears to exist.' ); - } else { + else WP_CLI::warning( 'Transient was not deleted; however, the transient does not appear to exist.' ); - } } } @@ -227,7 +199,7 @@ public function delete( $args, $assoc_args ) { * Determines the type of transients implementation. * * Indicates whether the transients API is using an object cache or the - * database. + * options table. * * For a more complete explanation of the transient cache, including the * network|site cache, please see docs for `wp transient`. @@ -235,583 +207,68 @@ public function delete( $args, $assoc_args ) { * ## EXAMPLES * * $ wp transient type - * Transients are saved to the database. + * Transients are saved to the wp_options table. */ public function type() { - if ( wp_using_ext_object_cache() ) { + global $_wp_using_ext_object_cache, $wpdb; + + if ( $_wp_using_ext_object_cache ) $message = 'Transients are saved to the object cache.'; - } else { - $message = 'Transients are saved to the database.'; - } + else + $message = 'Transients are saved to the ' . $wpdb->prefix . 'options table.'; WP_CLI::line( $message ); } - /** - * Lists transients and their values. - * - * ## OPTIONS - * - * [--search=] - * : Use wildcards ( * and ? ) to match transient name. - * - * [--exclude=] - * : Pattern to exclude. Use wildcards ( * and ? ) to match transient name. - * - * [--network] - * : Get the values of network|site transients. On single site, this is - * a specially-named cache key. On multisite, this is a global cache - * (instead of local to the site). - * - * [--unserialize] - * : Unserialize transient values in output. - * - * [--human-readable] - * : Human-readable output for expirations. - * - * [--fields=] - * : Limit the output to specific object fields. - * - * [--format=] - * : The serialization format for the value. - * --- - * default: table - * options: - * - table - * - json - * - csv - * - count - * - yaml - * --- - * - * ## AVAILABLE FIELDS - * - * This field will be displayed by default for each matching option: - * - * * name - * * value - * * expiration - * - * ## EXAMPLES - * - * # List all transients - * $ wp transient list - * +------+-------+---------------+ - * | name | value | expiration | - * +------+-------+---------------+ - * | foo | bar | 39 mins | - * | foo2 | bar2 | no expiration | - * | foo3 | bar2 | expired | - * | foo4 | bar4 | 4 hours | - * +------+-------+---------------+ - * - * @subcommand list - * - * @param string[] $args Positional arguments. Unused. - * @param array{search?: string, exclude?: string, network?: bool, unserialize?: bool, 'human-readable'?: bool, fields?: string, format?: string} $assoc_args Associative arguments. - */ - public function list_( $args, $assoc_args ) { - global $wpdb; - - if ( wp_using_ext_object_cache() ) { - WP_CLI::warning( 'Transients are stored in an external object cache, and this command only shows those stored in the database.' ); - } - - $network = Utils\get_flag_value( $assoc_args, 'network', false ); - $unserialize = Utils\get_flag_value( $assoc_args, 'unserialize', false ); - $human_readable = Utils\get_flag_value( $assoc_args, 'human-readable', false ); - - $fields = array( 'name', 'value', 'expiration' ); - if ( isset( $assoc_args['fields'] ) ) { - $fields = explode( ',', $assoc_args['fields'] ); - } - - $pattern = '%'; - $exclude = ''; - if ( isset( $assoc_args['search'] ) ) { - $pattern = Utils\esc_like( $assoc_args['search'] ); - // Substitute wildcards. - $pattern = str_replace( - array( '*', '?' ), - array( '%', '_' ), - $pattern - ); - } - if ( isset( $assoc_args['exclude'] ) ) { - $exclude = Utils\esc_like( $assoc_args['exclude'] ); - // Substitute wildcards. - $exclude = str_replace( - array( '*', '?' ), - array( '%', '_' ), - $exclude - ); - } - - if ( $network ) { - if ( is_multisite() ) { - $where = $wpdb->prepare( - 'WHERE `meta_key` LIKE %s', - Utils\esc_like( '_site_transient_' ) . $pattern - ); - $where .= $wpdb->prepare( - ' AND meta_key NOT LIKE %s', - Utils\esc_like( '_site_transient_timeout_' ) . '%' - ); - if ( $exclude ) { - $where .= $wpdb->prepare( - ' AND meta_key NOT LIKE %s', - Utils\esc_like( '_site_transient_' ) . $exclude - ); - } - - $query = "SELECT `meta_key` as `name`, `meta_value` as `value` FROM {$wpdb->sitemeta} {$where}"; - } else { - $where = $wpdb->prepare( - 'WHERE `option_name` LIKE %s', - Utils\esc_like( '_site_transient_' ) . $pattern - ); - $where .= $wpdb->prepare( - ' AND option_name NOT LIKE %s', - Utils\esc_like( '_site_transient_timeout_' ) . '%' - ); - if ( $exclude ) { - $where .= $wpdb->prepare( - ' AND option_name NOT LIKE %s', - Utils\esc_like( '_site_transient_' ) . $exclude - ); - } - - $query = "SELECT `option_name` as `name`, `option_value` as `value` FROM {$wpdb->options} {$where}"; - } - } else { - $where = $wpdb->prepare( - 'WHERE `option_name` LIKE %s', - Utils\esc_like( '_transient_' ) . $pattern - ); - $where .= $wpdb->prepare( - ' AND option_name NOT LIKE %s', - Utils\esc_like( '_transient_timeout_' ) . '%' - ); - if ( $exclude ) { - $where .= $wpdb->prepare( - ' AND option_name NOT LIKE %s', - Utils\esc_like( '_transient_' ) . $exclude - ); - } - - $query = "SELECT `option_name` as `name`, `option_value` as `value` FROM {$wpdb->options} {$where}"; - } - - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Prepared properly above. - $results = $wpdb->get_results( $query ); - - foreach ( $results as $result ) { - $result->name = str_replace( array( '_site_transient_', '_transient_' ), '', $result->name ); - $result->expiration = $this->get_transient_expiration( $result->name, $network, $human_readable ); - - if ( $unserialize ) { - $result->value = maybe_unserialize( $result->value ); - } - } - - $formatter = new \WP_CLI\Formatter( - $assoc_args, - $fields - ); - $formatter->display_items( $results ); - } - - /** - * Get a nested value from a transient. - * - * ## OPTIONS - * - * - * : Key for the transient. - * - * ... - * : The name(s) of the keys within the value to locate the value to pluck. - * - * [--format=] - * : The output format of the value. - * --- - * default: plaintext - * options: - * - plaintext - * - json - * - yaml - * --- - * - * [--network] - * : Get the value of a network|site transient. On single site, this is - * a specially-named cache key. On multisite, this is a global cache - * (instead of local to the site). - * - * @param string[] $args Positional arguments. - * @param array{format: string} $assoc_args Associative arguments. - */ - public function pluck( $args, $assoc_args ) { - list( $key ) = $args; - - $func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; - $value = $func( $key ); - - if ( false === $value ) { - WP_CLI::warning( 'Transient with key "' . $key . '" is not set.' ); - exit; - } - - $key_path = array_map( - function ( $key ) { - if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { - return (int) $key; - } - return $key; - }, - array_slice( $args, 1 ) - ); - - $traverser = new RecursiveDataStructureTraverser( $value ); - - try { - $value = $traverser->get( $key_path ); - } catch ( \Exception $e ) { - die( 1 ); - } - - WP_CLI::print_value( $value, $assoc_args ); - } - - /** - * Update a nested value from a transient. - * - * ## OPTIONS - * - * - * : Patch action to perform. - * --- - * options: - * - insert - * - update - * - delete - * --- - * - * - * : Key for the transient. - * - * ... - * : The name(s) of the keys within the value to locate the value to patch. - * - * [] - * : The new value. If omitted, the value is read from STDIN. - * - * [--format=] - * : The serialization format for the value. - * --- - * default: plaintext - * options: - * - plaintext - * - json - * --- - * - * [--expiration=] - * : Time until expiration, in seconds. - * --- - * default: 0 - * --- - * - * [--network] - * : Get the value of a network|site transient. On single site, this is - * a specially-named cache key. On multisite, this is a global cache - * (instead of local to the site). - * - * @param string[] $args Positional arguments. - * @param array{format: string} $assoc_args Associative arguments. - */ - public function patch( $args, $assoc_args ) { - list( $action, $key ) = $args; - - $expiration = (int) Utils\get_flag_value( $assoc_args, 'expiration', 0 ); - - $read_func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'get_site_transient' : 'get_transient'; - $write_func = Utils\get_flag_value( $assoc_args, 'network' ) ? 'set_site_transient' : 'set_transient'; - - $key_path = array_map( - function ( $key ) { - if ( is_numeric( $key ) && ( (string) intval( $key ) === $key ) ) { - return (int) $key; - } - - return $key; - }, - array_slice( $args, 2 ) - ); - - if ( 'delete' === $action ) { - $patch_value = null; - } else { - $stdin_value = Utils\has_stdin() - ? trim( WP_CLI::get_value_from_arg_or_stdin( $args, -1 ) ) - : null; - - if ( ! empty( $stdin_value ) ) { - $patch_value = WP_CLI::read_value( $stdin_value, $assoc_args ); - } elseif ( count( $key_path ) > 1 ) { - $patch_value = WP_CLI::read_value( (string) array_pop( $key_path ), $assoc_args ); - } else { - $patch_value = null; - } - - if ( null === $patch_value ) { - WP_CLI::error( 'Please provide value to update.' ); - } - } - - /* Need to make a copy of $current_value here as it is modified by reference */ - $old_value = $read_func( $key ); - $current_value = $old_value; - if ( is_object( $old_value ) ) { - $current_value = clone $old_value; - } - - $traverser = new RecursiveDataStructureTraverser( $current_value ); - - try { - $traverser->$action( $key_path, $patch_value ); - } catch ( \Exception $e ) { - WP_CLI::error( $e->getMessage() ); - } - - $patched_value = $traverser->value(); - - if ( $patched_value === $old_value ) { - WP_CLI::success( "Value passed for transient '$key' is unchanged." ); - } else { - $success = $write_func( $key, $patched_value, $expiration ); - if ( $success ) { - WP_CLI::success( "Updated transient '$key'." ); - } else { - WP_CLI::error( "Could not update transient '$key'." ); - } - } - } - - /** - * Retrieves the expiration time. - * - * @param string $name Transient name. - * @param bool $is_site_transient Optional. Whether this is a site transient. Default false. - * @param bool $human_readable Optional. Whether to return the difference between now and the - * expiration time in a human-readable format. Default false. - * @return string Expiration time string. - */ - private function get_transient_expiration( $name, $is_site_transient = false, $human_readable = false ) { - if ( $is_site_transient ) { - if ( is_multisite() ) { - /** - * @var string $expiration - */ - $expiration = get_site_option( '_site_transient_timeout_' . $name ); - } else { - /** - * @var string $expiration - */ - $expiration = get_option( '_site_transient_timeout_' . $name ); - } - } else { - /** - * @var string $expiration - */ - $expiration = get_option( '_transient_timeout_' . $name ); - } - - $expiration = (int) $expiration; - - if ( 0 === $expiration ) { - return $human_readable ? 'never expires' : 'false'; - } - - if ( ! $human_readable ) { - return (string) $expiration; - } - - $now = time(); - - if ( $now > $expiration ) { - return 'expired'; - } - - return human_time_diff( $now, $expiration ); - } - /** * Deletes all expired transients. - * - * Only deletes the expired transients from the database. - * - * @param bool $network Whether to delete transients or network|site transients. */ - private function delete_expired( $network ) { - global $wpdb; - - $count = 0; - - if ( ! $network ) { - $count += $wpdb->query( - $wpdb->prepare( - "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b - WHERE a.option_name LIKE %s - AND a.option_name NOT LIKE %s - AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) - AND b.option_value < %d", - Utils\esc_like( '_transient_' ) . '%', - Utils\esc_like( '_transient_timeout_' ) . '%', - time() - ) - ); - } elseif ( ! is_multisite() ) { - // Non-Multisite stores site transients in the options table. - $count += $wpdb->query( - $wpdb->prepare( - "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b - WHERE a.option_name LIKE %s - AND a.option_name NOT LIKE %s - AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) ) - AND b.option_value < %d", - Utils\esc_like( '_site_transient_' ) . '%', - Utils\esc_like( '_site_transient_timeout_' ) . '%', - time() - ) - ); - } else { - // Multisite stores site transients in the sitemeta table. - $count += $wpdb->query( - $wpdb->prepare( - "DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b - WHERE a.meta_key LIKE %s - AND a.meta_key NOT LIKE %s - AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) ) - AND b.meta_value < %d", - Utils\esc_like( '_site_transient_' ) . '%', - Utils\esc_like( '_site_transient_timeout_' ) . '%', - time() - ) - ); - } - - // The above queries delete the transient and the transient timeout - // thus each transient is counted twice. - $count = $count / 2; + private function delete_expired() { + global $wpdb, $_wp_using_ext_object_cache; + + // Always delete all transients from DB too. + $time = current_time('timestamp'); + $count = $wpdb->query( + "DELETE a, b FROM $wpdb->options a, $wpdb->options b WHERE + a.option_name LIKE '\_transient\_%' AND + a.option_name NOT LIKE '\_transient\_timeout\_%' AND + b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) + AND b.option_value < $time" + ); if ( $count > 0 ) { - WP_CLI::success( - sprintf( - '%d expired %s deleted from the database.', - $count, - Utils\pluralize( 'transient', $count ) - ) - ); + WP_CLI::success( "$count expired transients deleted from the database." ); } else { - WP_CLI::success( 'No expired transients found.' ); + WP_CLI::success( "No expired transients found." ); } - if ( wp_using_ext_object_cache() ) { - WP_CLI::warning( 'Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients.' ); + if ( $_wp_using_ext_object_cache ) { + WP_CLI::warning( 'Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients.'); } } /** * Deletes all transients. - * - * Only deletes the transients from the database. - * - * @param bool $network Whether to delete transients or network|site transients. */ - private function delete_all( $network ) { - global $wpdb; - - // To ensure proper count values we first delete all transients with a timeout - // and then the remaining transients without a timeout. - $count = 0; - - if ( ! $network ) { - $deleted = $wpdb->query( - $wpdb->prepare( - "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b - WHERE a.option_name LIKE %s - AND a.option_name NOT LIKE %s - AND b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) )", - Utils\esc_like( '_transient_' ) . '%', - Utils\esc_like( '_transient_timeout_' ) . '%' - ) - ); - - $count += $deleted / 2; // Ignore affected rows for timeouts. - - $count += $wpdb->query( - $wpdb->prepare( - "DELETE FROM $wpdb->options WHERE option_name LIKE %s", - Utils\esc_like( '_transient_' ) . '%' - ) - ); - } elseif ( ! is_multisite() ) { - // Non-Multisite stores site transients in the options table. - $deleted = $wpdb->query( - $wpdb->prepare( - "DELETE a, b FROM {$wpdb->options} a, {$wpdb->options} b - WHERE a.option_name LIKE %s - AND a.option_name NOT LIKE %s - AND b.option_name = CONCAT( '_site_transient_timeout_', SUBSTRING( a.option_name, 17 ) )", - Utils\esc_like( '_site_transient_' ) . '%', - Utils\esc_like( '_site_transient_timeout_' ) . '%' - ) - ); - - $count += $deleted / 2; // Ignore affected rows for timeouts. - - $count += $wpdb->query( - $wpdb->prepare( - "DELETE FROM $wpdb->options WHERE option_name LIKE %s", - Utils\esc_like( '_site_transient_' ) . '%' - ) - ); - } else { - // Multisite stores site transients in the sitemeta table. - $deleted = $wpdb->query( - $wpdb->prepare( - "DELETE a, b FROM {$wpdb->sitemeta} a, {$wpdb->sitemeta} b - WHERE a.meta_key LIKE %s - AND a.meta_key NOT LIKE %s - AND b.meta_key = CONCAT( '_site_transient_timeout_', SUBSTRING( a.meta_key, 17 ) )", - Utils\esc_like( '_site_transient_' ) . '%', - Utils\esc_like( '_site_transient_timeout_' ) . '%' - ) - ); - - $count += $deleted / 2; // Ignore affected rows for timeouts. - - $count += $wpdb->query( - $wpdb->prepare( - "DELETE FROM $wpdb->sitemeta WHERE meta_key LIKE %s", - Utils\esc_like( '_site_transient_' ) . '%' - ) - ); - } + private function delete_all() { + global $wpdb, $_wp_using_ext_object_cache; + + // Always delete all transients from DB too. + $count = $wpdb->query( + "DELETE FROM $wpdb->options + WHERE option_name LIKE '\_transient\_%' + OR option_name LIKE '\_site\_transient\_%'" + ); if ( $count > 0 ) { - WP_CLI::success( - sprintf( - '%d %s deleted from the database.', - $count, - Utils\pluralize( 'transient', $count ) - ) - ); + WP_CLI::success( "$count transients deleted from the database." ); } else { - WP_CLI::success( 'No transients found.' ); + WP_CLI::success( "No transients found." ); } - if ( wp_using_ext_object_cache() ) { - WP_CLI::warning( 'Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients.' ); + if ( $_wp_using_ext_object_cache ) { + WP_CLI::warning( 'Transients are stored in an external object cache, and this command only deletes those stored in the database. You must flush the cache to delete all transients.'); } } + } diff --git a/utils/behat-tags.php b/utils/behat-tags.php new file mode 100644 index 000000000..9c87fb656 --- /dev/null +++ b/utils/behat-tags.php @@ -0,0 +1,78 @@ +' ) +); + +# Skip Github API tests if `GITHUB_TOKEN` not available because of rate limiting. See https://github.com/wp-cli/wp-cli/issues/1612 +if ( ! getenv( 'GITHUB_TOKEN' ) ) { + $skip_tags[] = '@github-api'; +} + +# Skip tests known to be broken. +$skip_tags[] = '@broken'; + +# Require PHP extension, eg 'imagick'. +function extension_tags() { + $extension_tags = array(); + exec( "grep '@require-extension-[A-Za-z_]*' -h -o features/*.feature | uniq", $extension_tags ); + + $skip_tags = array(); + + $substr_start = strlen( '@require-extension-' ); + foreach ( $extension_tags as $tag ) { + $extension = substr( $tag, $substr_start ); + if ( ! extension_loaded( $extension ) ) { + $skip_tags[] = $tag; + } + } + + return $skip_tags; +} + +$skip_tags = array_merge( $skip_tags, extension_tags() ); + +if ( !empty( $skip_tags ) ) { + echo '--tags=~' . implode( '&&~', $skip_tags ); +} +