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 44abaac57..855744057 100644 --- a/.distignore +++ b/.distignore @@ -1,15 +1,11 @@ .DS_Store .git -.github .gitignore .gitlab-ci.yml .editorconfig .travis.yml behat.yml .circleci/config.yml -bitbucket-pipelines.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..e494d543b --- /dev/null +++ b/.github/settings.yml @@ -0,0 +1,40 @@ +# Used by Probot Settings: https://probot.github.io/apps/settings/ +repository: + description: Generates code for post types, taxonomies, blocks, plugins, child themes, etc. +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:scaffold + color: c5def5 + - name: command:scaffold-_s + color: c5def5 + - name: command:scaffold-block + color: c5def5 + - name: command:scaffold-child-theme + color: c5def5 + - name: command:scaffold-plugin + color: c5def5 + - name: command:scaffold-plugin-tests + color: c5def5 + - name: command:scaffold-post-type + color: c5def5 + - name: command:scaffold-taxonomy + color: c5def5 + - name: command:scaffold-theme-tests + 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..f3b908269 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,3 @@ 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..7d1184857 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,65 @@ +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 + - | + # Use phpunit 6.5.6 for PHP 7.2 temporarily until core tests are compat https://core.trac.wordpress.org/ticket/43218. + if [[ ${TRAVIS_PHP_VERSION:0:3} = "7.2" ]]; then + curl -sSfL -o $TRAVIS_BUILD_DIR/vendor/bin/phpunit https://phar.phpunit.de/phpunit-6.5.6.phar && chmod +x $TRAVIS_BUILD_DIR/vendor/bin/phpunit + fi + +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/README.md b/README.md index 195afac25..70c3471bb 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ wp-cli/scaffold-command Generates code for post types, taxonomies, blocks, plugins, child themes, etc. -[![Testing](https://github.com/wp-cli/scaffold-command/actions/workflows/testing.yml/badge.svg)](https://github.com/wp-cli/scaffold-command/actions/workflows/testing.yml) [![Code Coverage](https://codecov.io/gh/wp-cli/scaffold-command/branch/main/graph/badge.svg)](https://codecov.io/gh/wp-cli/scaffold-command/tree/main) +[![Build Status](https://travis-ci.org/wp-cli/scaffold-command.svg?branch=master)](https://travis-ci.org/wp-cli/scaffold-command) Quick links: [Using](#using) | [Installing](#installing) | [Contributing](#contributing) | [Support](#support) @@ -21,31 +21,29 @@ wp scaffold **EXAMPLES** - # Generate a new plugin with unit tests. + # Generate a new plugin with unit tests $ wp scaffold plugin sample-plugin Success: Created plugin files. Success: Created test files. - # Generate theme based on _s. + # Generate theme based on _s $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe" Success: Created theme 'Sample Theme'. - # Generate code for post type registration in given theme. + # Generate code for post type registration in given theme $ wp scaffold post-type movie --label=Movie --theme=simple-life - Success: Created '/var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php'. + Success: Created /var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php -### wp scaffold underscores +### wp scaffold _s Generates starter code for a theme based on _s. ~~~ -wp scaffold underscores [--activate] [--enable-network] [--theme_name=] [--author=<full-name>] [--author_uri=<uri>] [--sassify] [--woocommerce] [--force] +wp scaffold _s <slug> [--activate] [--enable-network] [--theme_name=<title>] [--author=<full-name>] [--author_uri=<uri>] [--sassify] [--force] ~~~ -**Alias:** `_s` - See the [Underscores website](https://underscores.me/) for more details. **OPTIONS** @@ -71,9 +69,6 @@ See the [Underscores website](https://underscores.me/) for more details. [--sassify] Include stylesheets as SASS. - [--woocommerce] - Include WooCommerce boilerplate files. - [--force] Overwrite files that already exist. @@ -93,11 +88,11 @@ Generates PHP, JS and CSS code for registering a Gutenberg block for a plugin or wp scaffold block <slug> [--title=<title>] [--dashicon=<dashicon>] [--category=<category>] [--theme] [--plugin=<plugin>] [--force] ~~~ -**Warning: `wp scaffold block` is deprecated.** +Blocks are the fundamental element of the Gutenberg editor. They are the primary way in which plugins and themes can register their own functionality and extend the capabilities of the editor. -The official script to generate a block is the [@wordpress/create-block](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package. +Visit the [Gutenberg handbook](https://wordpress.org/gutenberg/handbook/block-api/) to learn more about Block API. -See the [Create a Block tutorial](https://developer.wordpress.org/block-editor/getting-started/tutorial/) for a complete walk-through. +When you scaffold a block you must use either the theme or plugin option. The latter is recommended. **OPTIONS** @@ -131,6 +126,24 @@ See the [Create a Block tutorial](https://developer.wordpress.org/block-editor/g [--force] Overwrite files that already exist. +**EXAMPLES** + + # Generate a 'movie' block for the 'movies' plugin + $ wp scaffold block movie --title="Movie block" --plugin=movies + Success: Created block 'Movie block'. + + # Generate a 'movie' block for the 'simple-life' theme + $ wp scaffold block movie --title="Movie block" --theme=simple-life + Success: Created block 'Movie block'. + + # Create a new plugin and add two blocks + # Create plugin called books + $ wp scaffold plugin books + # Add a block called book to plugin books + $ wp scaffold block book --title="Book" --plugin=books + # Add a second block to plugin called books. + $ wp scaffold block books --title="Book List" --plugin=books + ### wp scaffold child-theme @@ -192,7 +205,7 @@ The following files are always generated: * `plugin-slug.php` is the main PHP plugin file. * `readme.txt` is the readme file for the plugin. -* `package.json` needed by NPM holds various metadata relevant to the project. Packages: `grunt`, `grunt-wp-i18n` and `grunt-wp-readme-to-markdown`. Scripts: `start`, `readme`, `i18n`. +* `package.json` needed by NPM holds various metadata relevant to the project. Packages: `grunt`, `grunt-wp-i18n` and `grunt-wp-readme-to-markdown`. * `Gruntfile.js` is the JS file containing Grunt tasks. Tasks: `i18n` containing `addtextdomain` and `makepot`, `readme` containing `wp_readme_to_markdown`. * `.editorconfig` is the configuration file for Editor. * `.gitignore` tells which files (or patterns) git should ignore. @@ -201,11 +214,11 @@ The following files are always generated: The following files are also included unless the `--skip-tests` is used: * `phpunit.xml.dist` is the configuration file for PHPUnit. -* `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service. +* `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite. * `tests/test-sample.php` is a sample file containing test cases. -* `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. +* `phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. **OPTIONS** @@ -236,12 +249,11 @@ The following files are also included unless the `--skip-tests` is used: [--ci=<provider>] Choose a configuration file for a continuous integration provider. --- - default: circle + default: travis options: + - travis - circle - gitlab - - bitbucket - - github --- [--activate] @@ -272,13 +284,13 @@ wp scaffold plugin-tests [<plugin>] [--dir=<dirname>] [--ci=<provider>] [--force The following files are generated by default: * `phpunit.xml.dist` is the configuration file for PHPUnit. -* `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service. +* `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite. * `tests/test-sample.php` is a sample file containing the actual tests. -* `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. +* `phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. -Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/). +Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/plugin-unit-tests/). **ENVIRONMENT** @@ -296,12 +308,11 @@ variable. [--ci=<provider>] Choose a configuration file for a continuous integration provider. --- - default: circle + default: travis options: + - travis - circle - gitlab - - bitbucket - - github --- [--force] @@ -323,8 +334,6 @@ Generates PHP code for registering a custom post type. wp scaffold post-type <slug> [--label=<label>] [--textdomain=<textdomain>] [--dashicon=<dashicon>] [--theme] [--plugin=<plugin>] [--raw] [--force] ~~~ -**Alias:** `cpt` - **OPTIONS** <slug> @@ -368,8 +377,6 @@ Generates PHP code for registering a custom taxonomy. wp scaffold taxonomy <slug> [--post_types=<post-types>] [--label=<label>] [--textdomain=<textdomain>] [--theme] [--plugin=<plugin>] [--raw] [--force] ~~~ -**Alias:** `tax` - **OPTIONS** <slug> @@ -415,13 +422,13 @@ wp scaffold theme-tests [<theme>] [--dir=<dirname>] [--ci=<provider>] [--force] The following files are generated by default: * `phpunit.xml.dist` is the configuration file for PHPUnit. -* `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service. +* `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * `tests/bootstrap.php` is the file that makes the current theme active when running the test suite. * `tests/test-sample.php` is a sample file containing the actual tests. -* `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. +* `phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. -Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/). +Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/plugin-unit-tests/). **ENVIRONMENT** @@ -439,12 +446,11 @@ variable. [--ci=<provider>] Choose a configuration file for a continuous integration provider. --- - default: circle + default: travis options: + - travis - circle - gitlab - - bitbucket - - github --- [--force] @@ -486,13 +492,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/codecov.yml b/codecov.yml deleted file mode 100644 index e69de29bb..000000000 diff --git a/composer.json b/composer.json index ca135714a..413b7fe2a 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,11 @@ { "name": "wp-cli/scaffold-command", - "type": "wp-cli-package", "description": "Generates code for post types, taxonomies, blocks, plugins, child themes, etc.", + "type": "wp-cli-package", "homepage": "https://github.com/wp-cli/scaffold-command", + "support": { + "issues": "https://github.com/wp-cli/scaffold-command/issues" + }, "license": "MIT", "authors": [ { @@ -11,31 +14,27 @@ "homepage": "https://runcommand.io" } ], - "require": { - "wp-cli/wp-cli": "^2.13" + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "": "src/" + }, + "files": [ "scaffold-command.php" ] }, + "require": {}, "require-dev": { - "wp-cli/extension-command": "^1.2 || ^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": "^1.5" }, "extra": { "branch-alias": { - "dev-main": "2.x-dev" + "dev-master": "1.x-dev" }, "bundled": true, "commands": [ "scaffold", - "scaffold underscores", + "scaffold _s", "scaffold block", "scaffold child-theme", "scaffold plugin", @@ -44,35 +43,5 @@ "scaffold taxonomy", "scaffold theme-tests" ] - }, - "autoload": { - "classmap": [ - "src/" - ], - "files": [ - "scaffold-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/scaffold-command/issues" } } diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php new file mode 100644 index 000000000..6f88ed1bb --- /dev/null +++ b/features/bootstrap/FeatureContext.php @@ -0,0 +1,942 @@ +<?php + +use Behat\Behat\Context\ClosuredContextInterface, + Behat\Behat\Context\TranslatedContextInterface, + Behat\Behat\Context\BehatContext, + Behat\Behat\Event\SuiteEvent; + +use \WP_CLI\Process; +use \WP_CLI\Utils; + +// Inside a community package +if ( file_exists( __DIR__ . '/utils.php' ) ) { + require_once __DIR__ . '/utils.php'; + require_once __DIR__ . '/Process.php'; + require_once __DIR__ . '/ProcessRun.php'; + $project_composer = dirname( dirname( dirname( __FILE__ ) ) ) . '/composer.json'; + if ( file_exists( $project_composer ) ) { + $composer = json_decode( file_get_contents( $project_composer ) ); + if ( ! empty( $composer->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. One is set on use: INVOKE_WP_CLI_WITH_PHP_ARGS-args. + * 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 '<file>.<line>'. 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' ); + $path_separator = Utils\is_windows() ? ';' : ':'; + $env = array( + 'PATH' => $bin_dir . $path_separator . $vendor_dir . $path_separator . 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 standard {VARIABLE_NAME} variables and the special {INVOKE_WP_CLI_WITH_PHP_ARGS-args} and {WP_VERSION-version-latest} variables. + * Note that standard variable names can only contain uppercase letters, digits and underscores and cannot begin with a digit. + */ + public function replace_variables( $str ) { + if ( false !== strpos( $str, '{INVOKE_WP_CLI_WITH_PHP_ARGS-' ) ) { + $str = $this->replace_invoke_wp_cli_with_php_args( $str ); + } + $str = preg_replace_callback( '/\{([A-Z_][A-Z_0-9]*)\}/', array( $this, 'replace_var' ), $str ); + if ( false !== strpos( $str, '{WP_VERSION-' ) ) { + $str = $this->replace_wp_versions( $str ); + } + return $str; + } + + /** + * Substitute {INVOKE_WP_CLI_WITH_PHP_ARGS-args} variables. + */ + private function replace_invoke_wp_cli_with_php_args( $str ) { + static $phar_path = null, $shell_path = null; + + if ( null === $phar_path ) { + $phar_path = false; + $phar_begin = '#!/usr/bin/env php'; + $phar_begin_len = strlen( $phar_begin ); + if ( ( $bin_dir = getenv( 'WP_CLI_BIN_DIR' ) ) && file_exists( $bin_dir . '/wp' ) && $phar_begin === file_get_contents( $bin_dir . '/wp', false, null, 0, $phar_begin_len ) ) { + $phar_path = $bin_dir . '/wp'; + } else { + $src_dir = dirname( dirname( __DIR__ ) ); + $bin_path = $src_dir . '/bin/wp'; + $vendor_bin_path = $src_dir . '/vendor/bin/wp'; + if ( file_exists( $bin_path ) && is_executable( $bin_path ) ) { + $shell_path = $bin_path; + } elseif ( file_exists( $vendor_bin_path ) && is_executable( $vendor_bin_path ) ) { + $shell_path = $vendor_bin_path; + } else { + $shell_path = 'wp'; + } + } + } + + $str = preg_replace_callback( '/{INVOKE_WP_CLI_WITH_PHP_ARGS-([^}]*)}/', function ( $matches ) use ( $phar_path, $shell_path ) { + return $phar_path ? "php {$matches[1]} {$phar_path}" : ( 'WP_CLI_PHP_ARGS=' . escapeshellarg( $matches[1] ) . ' ' . $shell_path ); + }, $str ); + + return $str; + } + + /** + * 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=<output_to>[,<num_top_processes>][,<num_top_scenarios>]" 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 "<grandparent-dir> <feature-file>:<line-number>", 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..584f679b9 --- /dev/null +++ b/features/bootstrap/Process.php @@ -0,0 +1,136 @@ +<?php + +namespace WP_CLI; + +use WP_CLI\Utils; + +/** + * Run a system process, and learn what happened. + */ +class Process { + /** + * @var string The full command to execute by the system. + */ + private $command; + + /** + * @var string|null The path of the working directory for the process or NULL if not specified (defaults to current working directory). + */ + private $cwd; + + /** + * @var array Environment variables to set when running the command. + */ + private $env; + + /** + * @var array Descriptor spec for `proc_open()`. + */ + private static $descriptors = array( + 0 => 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 = Utils\proc_open_compat( $this->command, self::$descriptors, $pipes, $this->cwd, $this->env ); + + $stdout = stream_get_contents( $pipes[1] ); + fclose( $pipes[1] ); + + $stderr = stream_get_contents( $pipes[2] ); + fclose( $pipes[2] ); + + $return_code = proc_close( $proc ); + + $run_time = microtime( true ) - $start_time; + + if ( self::$log_run_times ) { + if ( ! isset( self::$run_times[ $this->command ] ) ) { + self::$run_times[ $this->command ] = 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 @@ +<?php + +namespace WP_CLI; + +/** + * Results of an executed command. + */ +class ProcessRun { + /** + * @var string The full command executed by the system. + */ + public $command; + + /** + * @var string Captured output from the process' STDOUT. + */ + public $stdout; + + /** + * @var string Captured output from the process' STDERR. + */ + public $stderr; + + /** + * @var string|null The path of the working directory for the process or NULL if not specified (defaults to current working directory). + */ + public $cwd; + + /** + * @var array Environment variables set for this process. + */ + public $env; + + /** + * @var int Exit code of the process. + */ + public $return_code; + + /** + * @var float The run time of the process. + */ + public $run_time; + + /** + * @var array $props Properties of executed command. + */ + public function __construct( $props ) { + foreach ( $props as $key => $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 @@ +<?php + +// Utility functions used by Behat steps + +function assertRegExp( $regex, $actual ) { + if ( ! preg_match( $regex, $actual ) ) { + throw new Exception( "Actual value: " . var_export( $actual, true ) ); + } +} + +function assertEquals( $expected, $actual ) { + if ( $expected != $actual ) { + throw new Exception( "Actual value: " . var_export( $actual, true ) ); + } +} + +function assertNotEquals( $expected, $actual ) { + if ( $expected == $actual ) { + throw new Exception( "Actual value: " . var_export( $actual, true ) ); + } +} + +function assertNumeric( $actual ) { + if ( !is_numeric( $actual ) ) { + throw new Exception( "Actual value: " . var_export( $actual, true ) ); + } +} + +function assertNotNumeric( $actual ) { + if ( is_numeric( $actual ) ) { + throw new Exception( "Actual value: " . var_export( $actual, true ) ); + } +} + +function checkString( $output, $expected, $action, $message = false ) { + switch ( $action ) { + + case 'be': + $r = $expected === rtrim( $output, "\n" ); + break; + + case 'contain': + $r = false !== strpos( $output, $expected ); + break; + + case 'not contain': + $r = false === strpos( $output, $expected ); + break; + + default: + throw new Behat\Behat\Exception\PendingException(); + } + + if ( !$r ) { + if ( false === $message ) + $message = $output; + throw new Exception( $message ); + } +} + +function compareTables( $expected_rows, $actual_rows, $output ) { + // the first row is the header and must be present + if ( $expected_rows[0] != $actual_rows[0] ) { + throw new \Exception( $output ); + } + + unset( $actual_rows[0] ); + unset( $expected_rows[0] ); + + $missing_rows = array_diff( $expected_rows, $actual_rows ); + if ( !empty( $missing_rows ) ) { + throw new \Exception( $output ); + } +} + +function compareContents( $expected, $actual ) { + if ( gettype( $expected ) != gettype( $actual ) ) { + return false; + } + + if ( is_object( $expected ) ) { + foreach ( get_object_vars( $expected ) as $name => $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..26b3dbbed --- /dev/null +++ b/features/bootstrap/utils.php @@ -0,0 +1,1453 @@ +<?php + +// Utilities that do NOT depend on WordPress code. + +namespace WP_CLI\Utils; + +use \Composer\Semver\Comparator; +use \Composer\Semver\Semver; +use \WP_CLI; +use \WP_CLI\Dispatcher; +use \WP_CLI\Iterators\Transform; + +const PHAR_STREAM_PREFIX = 'phar://'; + +function inside_phar() { + return 0 === strpos( WP_CLI_ROOT, PHAR_STREAM_PREFIX ); +} + +// Files that need to be read by external programs have to be extracted from the Phar archive. +function extract_from_phar( $path ) { + if ( ! inside_phar() ) { + return $path; + } + + $fname = basename( $path ); + + $tmp_path = get_temp_dir() . "wp-cli-$fname"; + + copy( $path, $tmp_path ); + + register_shutdown_function( + function() use ( $tmp_path ) { + if ( file_exists( $tmp_path ) ) { + unlink( $tmp_path ); + } + } + ); + + return $tmp_path; +} + +function load_dependencies() { + if ( inside_phar() ) { + if ( file_exists( WP_CLI_ROOT . '/vendor/autoload.php' ) ) { + require WP_CLI_ROOT . '/vendor/autoload.php'; + } elseif ( file_exists( dirname( dirname( WP_CLI_ROOT ) ) . '/autoload.php' ) ) { + require dirname( dirname( WP_CLI_ROOT ) ) . '/autoload.php'; + } + return; + } + + $has_autoload = false; + + foreach ( get_vendor_paths() as $vendor_path ) { + if ( file_exists( $vendor_path . '/autoload.php' ) ) { + require $vendor_path . '/autoload.php'; + $has_autoload = true; + break; + } + } + + if ( ! $has_autoload ) { + fwrite( STDERR, "Internal error: Can't find Composer autoloader.\nTry running: composer install\n" ); + exit( 3 ); + } +} + +function get_vendor_paths() { + $vendor_paths = array( + WP_CLI_ROOT . '/../../../vendor', // part of a larger project / installed via Composer (preferred) + WP_CLI_ROOT . '/vendor', // top-level project / installed as Git clone + ); + $maybe_composer_json = WP_CLI_ROOT . '/../../../composer.json'; + if ( file_exists( $maybe_composer_json ) && is_readable( $maybe_composer_json ) ) { + $composer = json_decode( file_get_contents( $maybe_composer_json ) ); + if ( ! empty( $composer->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 = is_windows() ? 'notepad' : 'vi'; + } + + $descriptorspec = array( STDIN, STDOUT, STDERR ); + $process = proc_open_compat( "$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_compat( $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. + * @param int $interval Optional. The interval in milliseconds between updates. Default 100. + * @return cli\progress\Bar|WP_CLI\NoOp + */ +function make_progress_bar( $message, $count, $interval = 100 ) { + if ( \cli\Shell::isPiped() ) { + return new \WP_CLI\NoOp; + } + + return new \cli\progress\Bar( $message, $count, $interval ); +} + +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, '/\\' ) . '/'; +} + +/** + * Convert Windows EOLs to *nix. + * + * @param string $str String to convert. + * @return string String with carriage return / newline pairs reduced to newlines. + */ +function normalize_eols( $str ) { + return str_replace( "\r\n", "\n", $str ); +} + +/** + * 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; + } + + // `sys_get_temp_dir()` introduced PHP 5.2.1. Will always return something. + $temp = trailingslashit( sys_get_temp_dir() ); + + 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)([\'"]?)(.+?)(?<!\\\\)\1(?=$|\s)/', $arguments, $matches ); + $argv = isset( $matches[0] ) ? $matches[0] : array(); + $argv = array_map( + function( $arg ) { + foreach ( array( '"', "'" ) as $char ) { + if ( substr( $arg, 0, 1 ) === $char && substr( $arg, -1 ) === $char ) { + $arg = substr( $arg, 1, -1 ); + break; + } + } + return $arg; + }, $argv + ); + return $argv; +} + +/** + * Locale-independent version of basename() + * + * @access public + * + * @param string $path + * @param string $suffix + * @return string + */ +function basename( $path, $suffix = '' ) { + return urldecode( \basename( str_replace( array( '%2F', '%5C' ), '/', urlencode( $path ) ), $suffix ) ); +} + +/** + * Checks whether the output of the current script is a TTY or a pipe / redirect + * + * Returns true if STDOUT output is being redirected to a pipe or a file; false is + * output is being sent directly to the terminal. + * + * If an env variable SHELL_PIPE exists, returned result depends it's + * value. Strings like 1, 0, yes, no, that validate to booleans are accepted. + * + * To enable ASCII formatting even when shell is piped, use the + * ENV variable SHELL_PIPE=0 + * + * @access public + * + * @return bool + */ +// @codingStandardsIgnoreLine +function isPiped() { + $shellPipe = getenv( 'SHELL_PIPE' ); + + if ( false !== $shellPipe ) { + return filter_var( $shellPipe, FILTER_VALIDATE_BOOLEAN ); + } + + return (function_exists( 'posix_isatty' ) && ! posix_isatty( STDOUT )); +} + +/** + * Expand within paths to their matching paths. + * + * Has no effect on paths which do not use glob patterns. + * + * @param string|array $paths Single path as a string, or an array of paths. + * @param int $flags Optional. Flags to pass to glob. Defaults to GLOB_BRACE. + * + * @return array Expanded paths. + */ +function expand_globs( $paths, $flags = 'default' ) { + // Compatibility for systems without GLOB_BRACE. + $glob_func = 'glob'; + if ( 'default' === $flags ) { + if ( ! defined( 'GLOB_BRACE' ) || getenv( 'WP_CLI_TEST_EXPAND_GLOBS_NO_GLOB_BRACE' ) ) { + $glob_func = 'WP_CLI\Utils\glob_brace'; + } else { + $flags = GLOB_BRACE; + } + } + + $expanded = array(); + + foreach ( (array) $paths as $path ) { + $matching = array( $path ); + + if ( preg_match( '/[' . preg_quote( '*?[]{}!', '/' ) . ']/', $path ) ) { + $matching = $glob_func( $path, $flags ) ?: array(); + } + $expanded = array_merge( $expanded, $matching ); + } + + return array_values( array_unique( $expanded ) ); +} + +/** + * Simulate a `glob()` with the `GLOB_BRACE` flag set. For systems (eg Alpine Linux) built against a libc library (eg https://www.musl-libc.org/) that lacks it. + * Copied and adapted from Zend Framework's `Glob::fallbackGlob()` and Glob::nextBraceSub()`. + * + * Zend Framework (http://framework.zend.com/) + * + * @link http://github.com/zendframework/zf2 for the canonical source repository + * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) + * @license http://framework.zend.com/license/new-bsd New BSD License + * + * @param string $pattern Filename pattern. + * @param void $dummy_flags Not used. + * + * @return array Array of paths. + */ +function glob_brace( $pattern, $dummy_flags = null ) { + + static $next_brace_sub; + if ( ! $next_brace_sub ) { + // Find the end of the subpattern in a brace expression. + $next_brace_sub = function ( $pattern, $current ) { + $length = strlen( $pattern ); + $depth = 0; + + while ( $current < $length ) { + if ( '\\' === $pattern[ $current ] ) { + if ( ++$current === $length ) { + break; + } + $current++; + } else { + if ( ( '}' === $pattern[ $current ] && 0 === $depth-- ) || ( ',' === $pattern[ $current ] && 0 === $depth ) ) { + break; + } + + if ( '{' === $pattern[ $current++ ] ) { + $depth++; + } + } + } + + return $current < $length ? $current : null; + }; + } + + $length = strlen( $pattern ); + + // Find first opening brace. + for ( $begin = 0; $begin < $length; $begin++ ) { + if ( '\\' === $pattern[ $begin ] ) { + $begin++; + } elseif ( '{' === $pattern[ $begin ] ) { + break; + } + } + + // Find comma or matching closing brace. + if ( null === ( $next = $next_brace_sub( $pattern, $begin + 1 ) ) ) { + return glob( $pattern ); + } + + $rest = $next; + + // Point `$rest` to matching closing brace. + while ( '}' !== $pattern[ $rest ] ) { + if ( null === ( $rest = $next_brace_sub( $pattern, $rest + 1 ) ) ) { + return glob( $pattern ); + } + } + + $paths = array(); + $p = $begin + 1; + + // For each comma-separated subpattern. + do { + $subpattern = substr( $pattern, 0, $begin ) + . substr( $pattern, $p, $next - $p ) + . substr( $pattern, $rest + 1 ); + + if ( $result = glob_brace( $subpattern ) ) { + $paths = array_merge( $paths, $result ); + } + + if ( '}' === $pattern[ $next ] ) { + break; + } + + $p = $next + 1; + $next = $next_brace_sub( $pattern, $p ); + } while ( null !== $next ); + + return array_values( array_unique( $paths ) ); +} + +/** + * Get the closest suggestion for a mis-typed target term amongst a list of + * options. + * + * Uses the Levenshtein algorithm to calculate the relative "distance" between + * terms. + * + * If the "distance" to the closest term is higher than the threshold, an empty + * string is returned. + * + * @param string $target Target term to get a suggestion for. + * @param array $options Array with possible options. + * @param int $threshold Threshold above which to return an empty string. + * + * @return string + */ +function get_suggestion( $target, array $options, $threshold = 2 ) { + + $suggestion_map = array( + 'add' => 'create', + 'check' => 'check-update', + 'capability' => 'cap', + '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', + 'trash' => 'delete', + 'v' => 'version', + ); + + if ( array_key_exists( $target, $suggestion_map ) && in_array( $suggestion_map[ $target ], $options, true ) ) { + 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'; +} + +/** + * Windows compatible `proc_open()`. + * Works around bug in PHP, and also deals with *nix-like `ENV_VAR=blah cmd` environment variable prefixes. + * + * @access public + * + * @param string $command Command to execute. + * @param array $descriptorspec Indexed array of descriptor numbers and their values. + * @param array &$pipes Indexed array of file pointers that correspond to PHP's end of any pipes that are created. + * @param string $cwd Initial working directory for the command. + * @param array $env Array of environment variables. + * @param array $other_options Array of additional options (Windows only). + * + * @return string Command stripped of any environment variable settings. + */ +function proc_open_compat( $cmd, $descriptorspec, &$pipes, $cwd = null, $env = null, $other_options = null ) { + if ( is_windows() ) { + // Need to encompass the whole command in double quotes - PHP bug https://bugs.php.net/bug.php?id=49139 + $cmd = '"' . _proc_open_compat_win_env( $cmd, $env ) . '"'; + } + return proc_open( $cmd, $descriptorspec, $pipes, $cwd, $env, $other_options ); +} + +/** + * For use by `proc_open_compat()` only. Separated out for ease of testing. Windows only. + * Turns *nix-like `ENV_VAR=blah command` environment variable prefixes into stripped `cmd` with prefixed environment variables added to passed in environment array. + * + * @access private + * + * @param string $command Command to execute. + * @param array &$env Array of existing environment variables. Will be modified if any settings in command. + * + * @return string Command stripped of any environment variable settings. + */ +function _proc_open_compat_win_env( $cmd, &$env ) { + if ( false !== strpos( $cmd, '=' ) ) { + while ( preg_match( '/^([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|[^ ]*) /', $cmd, $matches ) ) { + $cmd = substr( $cmd, strlen( $matches[0] ) ); + if ( null === $env ) { + $env = array(); + } + $env[ $matches[1] ] = isset( $matches[2][0] ) && '"' === $matches[2][0] ? substr( $matches[2], 1, -1 ) : $matches[2]; + } + } + return $cmd; +} + +/** + * First half of escaping for LIKE special characters % and _ before preparing for MySQL. + * + * Use this only before wpdb::prepare() or esc_sql(). Reversing the order is very bad for security. + * + * Copied from core "wp-includes/wp-db.php". Avoids dependency on WP 4.4 wpdb. + * + * @access public + * + * @param string $text The raw text to be escaped. The input typed by the user should have no + * extra or deleted slashes. + * @return string Text in the form of a LIKE phrase. The output is not SQL safe. Call $wpdb::prepare() + * or real_escape next. + */ +function esc_like( $text ) { + return addcslashes( $text, '_%\\' ); +} + +/** + * Escapes (backticks) MySQL identifiers (aka schema object names) - i.e. column names, table names, and database/index/alias/view etc names. + * See https://dev.mysql.com/doc/refman/5.5/en/identifiers.html + * + * @param string|array $idents A single identifier or an array of identifiers. + * @return string|array An escaped string if given a string, or an array of escaped strings if given an array of strings. + */ +function esc_sql_ident( $idents ) { + $backtick = function ( $v ) { + // Escape any backticks in the identifier by doubling. + return '`' . str_replace( '`', '``', $v ) . '`'; + }; + if ( is_string( $idents ) ) { + return $backtick( $idents ); + } + return array_map( $backtick, $idents ); +} + +/** + * Check whether a given string is a valid JSON representation. + * + * @param string $argument String to evaluate. + * @param bool $ignore_scalars Optional. Whether to ignore scalar values. + * Defaults to true. + * + * @return bool Whether the provided string is a valid JSON representation. + */ +function is_json( $argument, $ignore_scalars = true ) { + if ( ! is_string( $argument ) || '' === $argument ) { + return false; + } + + if ( $ignore_scalars && ! in_array( $argument[0], array( '{', '[' ), true ) ) { + return false; + } + + json_decode( $argument, $assoc = true ); + + return json_last_error() === JSON_ERROR_NONE; +} + +/** + * Parse known shell arrays included in the $assoc_args array. + * + * @param array $assoc_args Associative array of arguments. + * @param array $array_arguments Array of argument keys that should receive an + * array through the shell. + * + * @return array + */ +function parse_shell_arrays( $assoc_args, $array_arguments ) { + if ( empty( $assoc_args ) || empty( $array_arguments ) ) { + return $assoc_args; + } + + foreach ( $array_arguments as $key ) { + if ( array_key_exists( $key, $assoc_args ) && is_json( $assoc_args[ $key ] ) ) { + $assoc_args[ $key ] = json_decode( $assoc_args[ $key ], $assoc = true ); + } + } + + return $assoc_args; +} diff --git a/features/extra/no-mail.php b/features/extra/no-mail.php new file mode 100644 index 000000000..de7a42272 --- /dev/null +++ b/features/extra/no-mail.php @@ -0,0 +1,7 @@ +<?php + +function wp_mail( $to ) { + // Log for testing purposes + WP_CLI::log( "WP-CLI test suite: Sent email to {$to}." ); +} + diff --git a/features/install-wp-tests.feature b/features/install-wp-tests.feature index 1fc931999..b5f8187d9 100644 --- a/features/install-wp-tests.feature +++ b/features/install-wp-tests.feature @@ -1,8 +1,6 @@ -# Note: You need to execute the mysql command `GRANT ALL PRIVILEGES ON wp_cli_test_scaffold.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "{DB_PASSWORD}";` for these tests to work locally. +# Note: You need to execute the mysql command `GRANT ALL PRIVILEGES ON wp_cli_test_scaffold.* TO "wp_cli_test"@"localhost" IDENTIFIED BY "password1";` for these tests to work locally. Feature: Scaffold install-wp-tests.sh tests - # TODO: Fix this on Windows. Fails because /usr/bin/env bash is not available. - @skip-windows Scenario: Help should be displayed Given a WP install And I run `wp plugin path` @@ -12,63 +10,34 @@ Feature: Scaffold install-wp-tests.sh tests When I try `/usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh` Then STDOUT should contain: """ - Usage: + usage: """ And the return code should be 1 - @less-than-php-8.0 @require-php-7.4 @require-mysql Scenario: Install latest version of WordPress Given a WP install - And a affirmative-response file: - """ - Y - """ - And a negative-response file: - """ - No - """ - And a get-phpunit-phar-url.php file: - """ - <?php - $version = 4; - if(PHP_VERSION_ID >= 50600) { - $version = 5; - } - if(PHP_VERSION_ID >= 70000) { - $version = 6; - } - if(PHP_VERSION_ID >= 70100) { - $version = 7; - } - if(PHP_VERSION_ID >= 80000) { - $version = 9; - } - echo "https://phar.phpunit.de/phpunit-{$version}.phar"; - """ - And I run `wp eval-file get-phpunit-phar-url.php --skip-wordpress` - And save STDOUT as {PHPUNIT_PHAR_URL} - And I run `curl -sS -L --fail -o phpunit {PHPUNIT_PHAR_URL}` And I run `wp plugin path` And save STDOUT as {PLUGIN_DIR} And I run `wp scaffold plugin hello-world` - # This throws a warning for the password provided via command line. - And I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I try `rm -fr /tmp/behat-wordpress-tests-lib` + And I try `rm -fr /tmp/behat-wordpress` - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest` + When I try `WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib WP_CORE_DIR=/tmp/behat-wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold wp_cli_test password1 localhost latest` Then the return code should be 0 - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ data """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ includes """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ wp-tests-config.php """ - And the {RUN_DIR}/wordpress directory should contain: + And the /tmp/behat-wordpress directory should contain: """ index.php license.txt @@ -96,227 +65,40 @@ Feature: Scaffold install-wp-tests.sh tests install_test_suite """ - # This throws a warning for the password provided via command line. - When I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "SHOW DATABASES"` - Then STDOUT should contain: - """ - wp_cli_test_scaffold - """ - - When I run `mkdir polyfills && composer init --name=test/package --require="yoast/phpunit-polyfills:^1" --no-interaction --quiet --working-dir=polyfills` - Then the return code should be 0 - - When I run `composer install --no-interaction --working-dir=polyfills --quiet` - Then the return code should be 0 - - When I run `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_TESTS_PHPUNIT_POLYFILLS_PATH={RUN_DIR}/polyfills/vendor/yoast/phpunit-polyfills php phpunit -c {PLUGIN_DIR}/hello-world/phpunit.xml.dist` + When I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "SHOW DATABASES"` Then the return code should be 0 - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest < affirmative-response` - Then the return code should be 0 - And STDERR should contain: - """ - Reinstalling - """ And STDOUT should contain: - """ - Database (wp_cli_test_scaffold) recreated. - """ - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest < negative-response` - Then the return code should be 0 - And STDERR should contain: - """ - Reinstalling - """ - And STDOUT should contain: - """ - Leaving the existing database (wp_cli_test_scaffold) in place - """ - - @require-php-8.0 @less-than-wp-5.8 - Scenario: Install latest version of WordPress on PHP 8.0+ and WordPress less then 5.8 - Given a WP install - And a affirmative-response file: - """ - Y - """ - And a negative-response file: - """ - No - """ - And a get-phpunit-phar-url.php file: - """ - <?php - $version = 4; - if(PHP_VERSION_ID >= 50600) { - $version = 5; - } - if(PHP_VERSION_ID >= 70000) { - $version = 6; - } - if(PHP_VERSION_ID >= 70100) { - $version = 7; - } - if(PHP_VERSION_ID >= 80000) { - $version = 9; - } - echo "https://phar.phpunit.de/phpunit-{$version}.phar"; - """ - And I run `wp eval-file get-phpunit-phar-url.php --skip-wordpress` - And save STDOUT as {PHPUNIT_PHAR_URL} - And I run `curl -sS --fail -L -o phpunit {PHPUNIT_PHAR_URL}` - And I run `wp plugin path` - And save STDOUT as {PLUGIN_DIR} - And I run `wp scaffold plugin hello-world` - # This throws a warning for the password provided via command line. - And I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest` - Then the return code should be 0 - And the {RUN_DIR}/wordpress-tests-lib directory should contain: - """ - data - """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: - """ - includes - """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: - """ - wp-tests-config.php - """ - And the {RUN_DIR}/wordpress directory should contain: - """ - index.php - license.txt - readme.html - wp-activate.php - wp-admin - wp-blog-header.php - wp-comments-post.php - wp-config-sample.php - wp-content - wp-cron.php - wp-includes - wp-links-opml.php - wp-load.php - wp-login.php - wp-mail.php - wp-settings.php - wp-signup.php - wp-trackback.php - xmlrpc.php - """ - And the {PLUGIN_DIR}/hello-world/phpunit.xml.dist file should exist - And STDERR should contain: - """ - install_test_suite - """ - - # This throws a warning for the password provided via command line. - When I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "SHOW DATABASES"` - Then STDOUT should contain: """ wp_cli_test_scaffold """ - When I run `mkdir polyfills && composer init --name=test/package --require="yoast/phpunit-polyfills:^1" --no-interaction --quiet --working-dir=polyfills` + When I run `WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib phpunit -c {PLUGIN_DIR}/hello-world/phpunit.xml.dist` Then the return code should be 0 - When I run `composer install --no-interaction --working-dir=polyfills --quiet` - Then the return code should be 0 - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_TESTS_PHPUNIT_POLYFILLS_PATH={RUN_DIR}/polyfills/vendor/yoast/phpunit-polyfills php phpunit -c {PLUGIN_DIR}/hello-world/phpunit.xml.dist` - Then the return code should be 1 - And STDOUT should contain: - """ - Looks like you're using PHPUnit 9.5. - """ - And STDOUT should contain: - """ - WordPress requires at least PHPUnit 5. - """ - And STDOUT should contain: - """ - and is currently only compatible with PHPUnit up to 7.x. - """ - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest < affirmative-response` - Then the return code should be 0 - And STDERR should contain: - """ - Reinstalling - """ - And STDOUT should contain: - """ - Database (wp_cli_test_scaffold) recreated. - """ - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest < negative-response` - Then the return code should be 0 - And STDERR should contain: - """ - Reinstalling - """ - And STDOUT should contain: - """ - Leaving the existing database (wp_cli_test_scaffold) in place - """ - - @require-php-8.0 @require-wp-5.8 @require-mysql - Scenario: Install latest version of WordPress on PHP 8.0+ and WordPress above 5.8 + Scenario: Install WordPress from trunk Given a WP install - And a affirmative-response file: - """ - Y - """ - And a negative-response file: - """ - No - """ - And a get-phpunit-phar-url.php file: - """ - <?php - $version = 4; - if(PHP_VERSION_ID >= 50600) { - $version = 5; - } - if(PHP_VERSION_ID >= 70000) { - $version = 6; - } - if(PHP_VERSION_ID >= 70100) { - $version = 7; - } - if(PHP_VERSION_ID >= 80000) { - $version = 9; - } - echo "https://phar.phpunit.de/phpunit-{$version}.phar"; - """ - And I run `wp eval-file get-phpunit-phar-url.php --skip-wordpress` - And save STDOUT as {PHPUNIT_PHAR_URL} - And I run `curl -sS -L --fail -o phpunit {PHPUNIT_PHAR_URL}` And I run `wp plugin path` And save STDOUT as {PLUGIN_DIR} And I run `wp scaffold plugin hello-world` - # This throws a warning for the password provided via command line. - And I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I try `rm -fr /tmp/behat-wordpress-tests-lib` + And I try `rm -fr /tmp/behat-wordpress` - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest` + When I try `WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib WP_CORE_DIR=/tmp/behat-wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold wp_cli_test password1 localhost trunk` Then the return code should be 0 - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ data """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ includes """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ wp-tests-config.php """ - And the {RUN_DIR}/wordpress directory should contain: + And the /tmp/behat-wordpress directory should contain: """ index.php license.txt @@ -338,175 +120,47 @@ Feature: Scaffold install-wp-tests.sh tests wp-trackback.php xmlrpc.php """ + And the contents of the /tmp/behat-wordpress/wp-includes/version.php file should match /\-(alpha|beta[0-9]+|RC[0-9]+)\-/ And the {PLUGIN_DIR}/hello-world/phpunit.xml.dist file should exist And STDERR should contain: """ install_test_suite """ - # This throws a warning for the password provided via command line. - When I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "SHOW DATABASES"` - Then STDOUT should contain: - """ - wp_cli_test_scaffold - """ - - When I run `mkdir polyfills && composer init --name=test/package --require="yoast/phpunit-polyfills:^1" --no-interaction --quiet --working-dir=polyfills` + When I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "SHOW DATABASES"` Then the return code should be 0 - - When I run `composer install --no-interaction --working-dir=polyfills --quiet` - Then the return code should be 0 - - When I run `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_TESTS_PHPUNIT_POLYFILLS_PATH={RUN_DIR}/polyfills/vendor/yoast/phpunit-polyfills php phpunit -c {PLUGIN_DIR}/hello-world/phpunit.xml.dist` - Then the return code should be 0 - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest < affirmative-response` - Then the return code should be 0 - And STDERR should contain: - """ - Reinstalling - """ And STDOUT should contain: - """ - Database (wp_cli_test_scaffold) recreated. - """ - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest < negative-response` - Then the return code should be 0 - And STDERR should contain: - """ - Reinstalling - """ - And STDOUT should contain: - """ - Leaving the existing database (wp_cli_test_scaffold) in place - """ - - @require-php-7.4 @require-mysql - Scenario: Install WordPress from trunk - Given a WP install - And a get-phpunit-phar-url.php file: - """ - <?php - $version = 4; - if(PHP_VERSION_ID >= 50600) { - $version = 5; - } - if(PHP_VERSION_ID >= 70000) { - $version = 6; - } - if(PHP_VERSION_ID >= 70100) { - $version = 7; - } - if(PHP_VERSION_ID >= 80000) { - $version = 9; - } - echo "https://phar.phpunit.de/phpunit-{$version}.phar"; - """ - And I run `wp eval-file get-phpunit-phar-url.php --skip-wordpress` - And save STDOUT as {PHPUNIT_PHAR_URL} - And I run `curl -sS -f -L -o phpunit {PHPUNIT_PHAR_URL}` - And I run `wp plugin path` - And save STDOUT as {PLUGIN_DIR} - And I run `wp scaffold plugin hello-world` - # This throws a warning for the password provided via command line. - And I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` - - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} trunk` - Then the return code should be 0 - And the {RUN_DIR}/wordpress-tests-lib directory should contain: - """ - data - """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: - """ - includes - """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: - """ - wp-tests-config.php - """ - And the {RUN_DIR}/wordpress directory should contain: - """ - index.php - """ - - # WP 5.0+: js - - And the {RUN_DIR}/wordpress directory should contain: - """ - license.txt - readme.html - """ - - # WP 5.0+: styles - - And the {RUN_DIR}/wordpress directory should contain: - """ - wp-activate.php - wp-admin - wp-blog-header.php - wp-comments-post.php - wp-config-sample.php - wp-content - wp-cron.php - wp-includes - wp-links-opml.php - wp-load.php - wp-login.php - wp-mail.php - wp-settings.php - wp-signup.php - wp-trackback.php - xmlrpc.php - """ - And the contents of the {RUN_DIR}/wordpress/wp-includes/version.php file should match /\-(alpha|beta[0-9]+|RC[0-9]+)\-/ - And the {PLUGIN_DIR}/hello-world/phpunit.xml.dist file should exist - And STDERR should contain: - """ - install_test_suite - """ - - # This throws a warning for the password provided via command line. - When I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "SHOW DATABASES"` - Then STDOUT should contain: """ wp_cli_test_scaffold """ - When I run `composer init --no-interaction --quiet --name=wp-cli/test-scenario --require="yoast/phpunit-polyfills=^1.0.1" --working-dir={RUN_DIR}/wordpress-tests-lib` + When I run `WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib phpunit -c {PLUGIN_DIR}/hello-world/phpunit.xml.dist` Then the return code should be 0 - When I run `composer install --no-interaction --quiet --working-dir={RUN_DIR}/wordpress-tests-lib` - Then the return code should be 0 - - When I run `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_TESTS_PHPUNIT_POLYFILLS_PATH={RUN_DIR}/wordpress-tests-lib/vendor/yoast/phpunit-polyfills php phpunit -c {PLUGIN_DIR}/hello-world/phpunit.xml.dist` - Then the return code should be 0 - - @require-mysql Scenario: Install WordPress 3.7 and phpunit will not run Given a WP install And I run `wp plugin path` And save STDOUT as {PLUGIN_DIR} And I run `wp scaffold plugin hello-world` - # This throws a warning for the password provided via command line. - And I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I try `rm -fr /tmp/behat-wordpress-tests-lib` + And I try `rm -fr /tmp/behat-wordpress` - When I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} 3.7` + When I try `WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib WP_CORE_DIR=/tmp/behat-wordpress /usr/bin/env bash {PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh wp_cli_test_scaffold wp_cli_test password1 localhost 3.7` Then the return code should be 0 - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ data """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ includes """ - And the {RUN_DIR}/wordpress-tests-lib directory should contain: + And the /tmp/behat-wordpress-tests-lib directory should contain: """ wp-tests-config.php """ - And the {RUN_DIR}/wordpress directory should contain: + And the /tmp/behat-wordpress directory should contain: """ index.php license.txt @@ -528,7 +182,7 @@ Feature: Scaffold install-wp-tests.sh tests wp-trackback.php xmlrpc.php """ - And the {RUN_DIR}/wordpress/wp-includes/version.php file should contain: + And the /tmp/behat-wordpress/wp-includes/version.php file should contain: """ 3.7 """ @@ -537,8 +191,8 @@ Feature: Scaffold install-wp-tests.sh tests install_test_suite """ - # This throws a warning for the password provided via command line. - When I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "SHOW DATABASES"` + When I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "SHOW DATABASES"` + Then the return code should be 0 And STDOUT should contain: """ wp_cli_test_scaffold diff --git a/features/scaffold-block.feature b/features/scaffold-block.feature index 3481741ea..61c7669ca 100644 --- a/features/scaffold-block.feature +++ b/features/scaffold-block.feature @@ -2,13 +2,14 @@ Feature: WordPress block code scaffolding Background: Given a WP install - And I run `wp scaffold plugin movies` + Given I run `wp scaffold plugin movies` And I run `wp plugin path movies --dir` And save STDOUT as {PLUGIN_DIR} - And I run `wp theme install twentytwelve --activate --force` - And I run `wp theme path twentytwelve --dir` + Given I run `wp theme install p2 --activate` + And I run `wp theme path p2 --dir` And save STDOUT as {THEME_DIR} + Scenario: Scaffold a block with an invalid slug When I try `wp scaffold block The_Godfather` Then STDERR should be: @@ -30,13 +31,6 @@ Feature: WordPress block code scaffolding Error: Can't find 'unknown' plugin. """ - Scenario: Scaffold a block for an invalid plugin slug - When I try `wp scaffold block some-block --plugin=plugin.name.with.dots` - Then STDERR should contain: - """ - Error: Invalid plugin name specified. - """ - Scenario: Scaffold a block for a specific plugin When I run `wp scaffold block the-green-mile --plugin=movies` Then the {PLUGIN_DIR}/blocks/the-green-mile.php file should exist @@ -46,7 +40,7 @@ Feature: WordPress block code scaffolding """ And the {PLUGIN_DIR}/blocks/the-green-mile.php file should contain: """ - index_js = 'the-green-mile/index.js'; + $block_js = 'the-green-mile/block.js'; """ And the {PLUGIN_DIR}/blocks/the-green-mile.php file should contain: """ @@ -58,36 +52,32 @@ Feature: WordPress block code scaffolding """ And the {PLUGIN_DIR}/blocks/the-green-mile.php file should contain: """ - register_block_type( - """ - And the {PLUGIN_DIR}/blocks/the-green-mile.php file should contain: - """ - 'movies/the-green-mile', + register_block_type( 'movies/the-green-mile', array( """ And the {PLUGIN_DIR}/blocks/the-green-mile.php file should contain: """ add_action( 'init', 'the_green_mile_block_init' ); """ - And the {PLUGIN_DIR}/blocks/the-green-mile/index.js file should exist - And the {PLUGIN_DIR}/blocks/the-green-mile/index.js file should contain: + And the {PLUGIN_DIR}/blocks/the-green-mile/block.js file should exist + And the {PLUGIN_DIR}/blocks/the-green-mile/block.js file should contain: """ registerBlockType( 'movies/the-green-mile', { """ - And the {PLUGIN_DIR}/blocks/the-green-mile/index.js file should contain: + And the {PLUGIN_DIR}/blocks/the-green-mile/block.js file should contain: """ - title: __( 'The green mile', 'movies' ), + title: __( 'The green mile' ), """ - And the {PLUGIN_DIR}/blocks/the-green-mile/index.js file should contain: + And the {PLUGIN_DIR}/blocks/the-green-mile/block.js file should contain: """ category: 'widgets', """ - And the {PLUGIN_DIR}/blocks/the-green-mile/index.js file should contain: + And the {PLUGIN_DIR}/blocks/the-green-mile/block.js file should contain: """ - __( 'Hello from the editor!', 'movies' ) + __( 'Hello from the editor!' ) """ - And the {PLUGIN_DIR}/blocks/the-green-mile/index.js file should contain: + And the {PLUGIN_DIR}/blocks/the-green-mile/block.js file should contain: """ - __( 'Hello from the saved content!', 'movies' ) + __( 'Hello from the saved content!' ) """ And the {PLUGIN_DIR}/blocks/the-green-mile/editor.css file should exist And the {PLUGIN_DIR}/blocks/the-green-mile/editor.css file should contain: @@ -106,9 +96,9 @@ Feature: WordPress block code scaffolding Scenario: Scaffold a block with a specific title provided When I run `wp scaffold block shawshank-redemption --plugin=movies --title="The Shawshank Redemption"` - Then the {PLUGIN_DIR}/blocks/shawshank-redemption/index.js file should contain: + Then the {PLUGIN_DIR}/blocks/shawshank-redemption/block.js file should contain: """ - title: __( 'The Shawshank Redemption', 'movies' ), + title: __( 'The Shawshank Redemption' ), """ And STDOUT should be: """ @@ -117,7 +107,7 @@ Feature: WordPress block code scaffolding Scenario: Scaffold a block with a specific dashicon provided When I run `wp scaffold block forrest-gump --plugin=movies --dashicon=movie` - Then the {PLUGIN_DIR}/blocks/forrest-gump/index.js file should contain: + Then the {PLUGIN_DIR}/blocks/forrest-gump/block.js file should contain: """ icon: 'movie', """ @@ -128,7 +118,7 @@ Feature: WordPress block code scaffolding Scenario: Scaffold a block with a specific category provided When I run `wp scaffold block pulp-fiction --plugin=movies --category=embed` - Then the {PLUGIN_DIR}/blocks/pulp-fiction/index.js file should contain: + Then the {PLUGIN_DIR}/blocks/pulp-fiction/block.js file should contain: """ category: 'embed', """ @@ -140,7 +130,7 @@ Feature: WordPress block code scaffolding Scenario: Scaffold a block for an active theme When I run `wp scaffold block fight-club --theme` Then the {THEME_DIR}/blocks/fight-club.php file should exist - And the {THEME_DIR}/blocks/fight-club/index.js file should exist + And the {THEME_DIR}/blocks/fight-club/block.js file should exist And the {THEME_DIR}/blocks/fight-club/editor.css file should exist And the {THEME_DIR}/blocks/fight-club/style.css file should exist And STDOUT should be: @@ -156,33 +146,12 @@ Feature: WordPress block code scaffolding """ Scenario: Scaffold a block for a specific theme - When I run `wp scaffold block intouchables --theme=twentytwelve` + When I run `wp scaffold block intouchables --theme=p2` Then the {THEME_DIR}/blocks/intouchables.php file should exist - And the {THEME_DIR}/blocks/intouchables/index.js file should exist + And the {THEME_DIR}/blocks/intouchables/block.js file should exist And the {THEME_DIR}/blocks/intouchables/editor.css file should exist And the {THEME_DIR}/blocks/intouchables/style.css file should exist And STDOUT should be: """ Success: Created block 'Intouchables'. """ - - Scenario: Plugin- or theme-specific functions are only used in the correct context - When I run `wp scaffold block plugin-block --plugin=movies` - And I run `wp scaffold block theme-block --theme=twentytwelve` - Then the {PLUGIN_DIR}/blocks/plugin-block.php file should contain: - """ - plugins_url - """ - And the {PLUGIN_DIR}/blocks/plugin-block.php file should not contain: - """ - get_stylesheet_directory - """ - And the {THEME_DIR}/blocks/theme-block.php file should contain: - """ - get_stylesheet_directory - """ - And the {THEME_DIR}/blocks/theme-block.php file should not contain: - """ - plugins_url - """ - diff --git a/features/scaffold-lint.feature b/features/scaffold-lint.feature deleted file mode 100644 index 2bc7c1b85..000000000 --- a/features/scaffold-lint.feature +++ /dev/null @@ -1,85 +0,0 @@ -Feature: Lint scaffolded code - - Background: - Given a WP install - And I run `wp plugin path` - And save STDOUT as {PLUGIN_DIR} - - # Create a helper plugin to install phpcs once for all scenarios - When I run `wp scaffold plugin phpcs-helper --skip-tests` - Then the return code should be 0 - - # Install coding standards - When I run `composer config --working-dir={PLUGIN_DIR}/phpcs-helper allow-plugins.dealerdirect/phpcodesniffer-composer-installer true` - Then the return code should be 0 - - When I run `composer require --dev --working-dir={PLUGIN_DIR}/phpcs-helper dealerdirect/phpcodesniffer-composer-installer wp-coding-standards/wpcs --no-interaction --quiet` - Then the return code should be 0 - - Scenario: Scaffold plugin and lint it - When I run `wp scaffold plugin test-plugin` - Then STDOUT should not be empty - And the {PLUGIN_DIR}/test-plugin/test-plugin.php file should exist - And the {PLUGIN_DIR}/test-plugin/.phpcs.xml.dist file should exist - - When I run `{PLUGIN_DIR}/phpcs-helper/vendor/bin/phpcs --standard=WordPress {PLUGIN_DIR}/test-plugin/test-plugin.php` - Then the return code should be 0 - - Scenario: Scaffold post-type and lint it - When I run `wp theme install twentytwentyone --force --activate` - And I run `wp eval 'echo STYLESHEETPATH;'` - And save STDOUT as {STYLESHEETPATH} - - And I run `wp scaffold post-type movie --theme` - Then STDOUT should not be empty - And the {STYLESHEETPATH}/post-types/movie.php file should exist - - When I run `{PLUGIN_DIR}/phpcs-helper/vendor/bin/phpcs --standard=WordPress {STYLESHEETPATH}/post-types/movie.php` - Then the return code should be 0 - - Scenario: Scaffold taxonomy and lint it - When I run `wp theme install twentytwentyone --force --activate` - And I run `wp eval 'echo STYLESHEETPATH;'` - And save STDOUT as {STYLESHEETPATH} - - And I run `wp scaffold taxonomy genre --theme` - Then STDOUT should not be empty - And the {STYLESHEETPATH}/taxonomies/genre.php file should exist - - When I run `{PLUGIN_DIR}/phpcs-helper/vendor/bin/phpcs --standard=WordPress {STYLESHEETPATH}/taxonomies/genre.php` - Then the return code should be 0 - - Scenario: Scaffold plugin tests and lint them - When I run `wp scaffold plugin test-plugin` - Then STDOUT should not be empty - And the {PLUGIN_DIR}/test-plugin/tests directory should exist - And the {PLUGIN_DIR}/test-plugin/tests/bootstrap.php file should exist - And the {PLUGIN_DIR}/test-plugin/tests/test-sample.php file should exist - - # Run phpcs on the test files - When I run `{PLUGIN_DIR}/phpcs-helper/vendor/bin/phpcs --standard=WordPress {PLUGIN_DIR}/test-plugin/tests/bootstrap.php {PLUGIN_DIR}/test-plugin/tests/test-sample.php` - Then the return code should be 0 - - Scenario: Scaffold child theme and lint it - When I run `wp theme install twentytwentyone --force --activate` - And I run `wp theme path` - And save STDOUT as {THEME_DIR} - - And I run `wp scaffold child-theme test-child --parent_theme=twentytwentyone` - Then STDOUT should not be empty - And the {THEME_DIR}/test-child/functions.php file should exist - - When I run `{PLUGIN_DIR}/phpcs-helper/vendor/bin/phpcs --standard=WordPress {THEME_DIR}/test-child/functions.php` - Then the return code should be 0 - - Scenario: Scaffold block and lint it - When I run `wp scaffold plugin movies` - And I run `wp plugin path movies --dir` - And save STDOUT as {MOVIES_DIR} - - And I run `wp scaffold block the-green-mile --plugin=movies` - Then STDOUT should not be empty - And the {MOVIES_DIR}/blocks/the-green-mile.php file should exist - - When I run `{PLUGIN_DIR}/phpcs-helper/vendor/bin/phpcs --standard=WordPress {MOVIES_DIR}/blocks/the-green-mile.php` - Then the return code should be 0 diff --git a/features/scaffold-plugin-tests.feature b/features/scaffold-plugin-tests.feature index b224dbd30..a4bfa5eb8 100644 --- a/features/scaffold-plugin-tests.feature +++ b/features/scaffold-plugin-tests.feature @@ -1,7 +1,5 @@ Feature: Scaffold plugin unit tests - # TODO: Fix this on Windows. Fails because is_executable() fails for .sh files. - @skip-windows Scenario: Scaffold plugin tests Given a WP install When I run `wp plugin path` @@ -23,7 +21,7 @@ Feature: Scaffold plugin unit tests """ And the {PLUGIN_DIR}/hello-world/tests/bootstrap.php file should contain: """ - require dirname( __DIR__ ) . '/hello-world.php'; + require dirname( dirname( __FILE__ ) ) . '/hello-world.php'; """ And the {PLUGIN_DIR}/hello-world/tests/bootstrap.php file should contain: """ @@ -37,35 +35,42 @@ Feature: Scaffold plugin unit tests """ install-wp-tests.sh """ - And the {PLUGIN_DIR}/hello-world/phpunit.xml.dist file should contain: - """ - <exclude>./tests/test-sample.php</exclude> - """ - And the {PLUGIN_DIR}/hello-world/.phpcs.xml.dist file should exist + And the {PLUGIN_DIR}/hello-world/phpunit.xml.dist file should exist + And the {PLUGIN_DIR}/hello-world/phpcs.xml.dist file should exist And the {PLUGIN_DIR}/hello-world/circle.yml file should not exist - And the {PLUGIN_DIR}/hello-world/bitbucket-pipelines.yml file should not exist + And the {PLUGIN_DIR}/hello-world/.circleci directory should not exist And the {PLUGIN_DIR}/hello-world/.gitlab-ci.yml file should not exist - And the {PLUGIN_DIR}/hello-world/.circleci/config.yml file should contain: - """ - jobs: - php56-build: - <<: *php_job - docker: - - image: circleci/php:5.6 - - image: *mysql_image - """ - And the {PLUGIN_DIR}/hello-world/.circleci/config.yml file should contain: - """ - workflows: - version: 2 - main: - jobs: - - php56-build - - php70-build - - php71-build - - php72-build - - php73-build - - php74-build + And the {PLUGIN_DIR}/hello-world/.travis.yml file should contain: + """ + script: + - | + if [[ ! -z "$WP_VERSION" ]] ; then + phpunit + WP_MULTISITE=1 phpunit + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + phpcs + fi + """ + And the {PLUGIN_DIR}/hello-world/.travis.yml file should contain: + """ + matrix: + include: + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=4.5 + - php: 5.6 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=trunk + - php: 5.6 + env: WP_TRAVISCI=phpcs + - php: 5.3 + env: WP_VERSION=latest """ When I run `wp eval "if ( is_executable( '{PLUGIN_DIR}/hello-world/bin/install-wp-tests.sh' ) ) { echo 'executable'; } else { exit( 1 ); }"` @@ -80,6 +85,7 @@ Feature: Scaffold plugin unit tests When I run `wp plugin path hello-world --dir` Then save STDOUT as {PLUGIN_DIR} + And the {PLUGIN_DIR}/.travis.yml file should not exist And the {PLUGIN_DIR}/circle.yml file should not exist And the {PLUGIN_DIR}/.circleci/config.yml file should contain: """ @@ -97,18 +103,6 @@ Feature: Scaffold plugin unit tests """ php71-build """ - And the {PLUGIN_DIR}/.circleci/config.yml file should contain: - """ - php72-build - """ - And the {PLUGIN_DIR}/.circleci/config.yml file should contain: - """ - php73-build - """ - And the {PLUGIN_DIR}/.circleci/config.yml file should contain: - """ - php74-build - """ Scenario: Scaffold plugin tests with Circle as the provider, part two Given a WP install @@ -119,6 +113,7 @@ Feature: Scaffold plugin unit tests When I run `wp scaffold plugin-tests hello-world --ci=circle` Then STDOUT should not be empty + And the {PLUGIN_DIR}/.travis.yml file should not exist And the {PLUGIN_DIR}/circle.yml file should not exist And the {PLUGIN_DIR}/.circleci/config.yml file should contain: """ @@ -152,77 +147,21 @@ Feature: Scaffold plugin unit tests When I run `wp scaffold plugin-tests hello-world --ci=gitlab` Then STDOUT should not be empty + And the {PLUGIN_DIR}/.travis.yml file should not exist And the {PLUGIN_DIR}/.gitlab-ci.yml file should contain: """ MYSQL_DATABASE """ - Scenario: Scaffold plugin tests with Bitbucket Pipelines as the provider - Given a WP install - And I run `wp scaffold plugin hello-world --skip-tests` - - When I run `wp plugin path hello-world --dir` - Then save STDOUT as {PLUGIN_DIR} - - When I run `wp scaffold plugin-tests hello-world --ci=bitbucket` - Then STDOUT should not be empty - And the {PLUGIN_DIR}/bitbucket-pipelines.yml file should contain: - """ - pipelines: - default: - """ - And the {PLUGIN_DIR}/bitbucket-pipelines.yml file should contain: - """ - - step: - image: php:7.4 - name: "PHP 7.4" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - """ - And the {PLUGIN_DIR}/bitbucket-pipelines.yml file should contain: - """ - - step: - image: php:8.0 - name: "PHP 8.0" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - """ - And the {PLUGIN_DIR}/bitbucket-pipelines.yml file should contain: - """ - - step: - image: php:8.2 - name: "PHP 8.2" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - """ - And the {PLUGIN_DIR}/bitbucket-pipelines.yml file should contain: - """ - definitions: - services: - database: - image: mysql:latest - environment: - MYSQL_DATABASE: 'wordpress_tests' - MYSQL_ROOT_PASSWORD: 'root' - """ - Scenario: Scaffold plugin tests with invalid slug Given a WP install - And a wp-content/plugins/foo.php file: + Then the {RUN_DIR}/wp-content/plugins/hello.php file should exist + + When I try `wp scaffold plugin-tests hello` + Then STDERR should be: """ - <?php - /** - * Plugin Name: Foo - * Description: Foo plugin - */ + Error: Invalid plugin slug specified. No such target directory '{RUN_DIR}/wp-content/plugins/hello'. """ - Then the {RUN_DIR}/wp-content/plugins/foo.php file should exist - - When I try `wp scaffold plugin-tests foo` - Then STDERR should match #Error: Invalid plugin slug specified\. No such target directory '.*wp-content/plugins/foo'\.# And the return code should be 1 When I try `wp scaffold plugin-tests .` @@ -235,21 +174,7 @@ Feature: Scaffold plugin unit tests When I try `wp scaffold plugin-tests ../` Then STDERR should be: """ - Error: Invalid plugin slug specified. The slug can only contain alphanumeric characters, underscores, and dashes. - """ - And the return code should be 1 - - When I try `wp scaffold plugin-tests my-plugin/` - Then STDERR should be: - """ - Error: Invalid plugin slug specified. The slug can only contain alphanumeric characters, underscores, and dashes. - """ - And the return code should be 1 - - When I try `wp scaffold plugin-tests my-plugin\\` - Then STDERR should be: - """ - Error: Invalid plugin slug specified. The slug can only contain alphanumeric characters, underscores, and dashes. + Error: Invalid plugin slug specified. The target directory '{RUN_DIR}/wp-content/plugins/../' is not in '{RUN_DIR}/wp-content/plugins'. """ And the return code should be 1 @@ -270,7 +195,10 @@ Feature: Scaffold plugin unit tests When I run `rm -rf {PLUGIN_DIR} && touch {PLUGIN_DIR}` Then the return code should be 0 When I try `wp scaffold plugin-tests hello-world` - Then STDERR should match #Error: Invalid plugin slug specified\. No such target directory '.*hello-world'\.# + Then STDERR should be: + """ + Error: Invalid plugin slug specified. No such target directory '{PLUGIN_DIR}'. + """ And the return code should be 1 Scenario: Scaffold plugin tests with a symbolic link @@ -289,41 +217,3 @@ Feature: Scaffold plugin unit tests """ bootstrap.php """ - - Scenario: Scaffold plugin tests with custom main file - Given a WP install - And a wp-content/plugins/foo/bar.php file: - """ - <?php - /** - * Plugin Name: Foo - * Plugin URI: https://example.com - * Description: Foo description - * Author: John Doe - * Author URI: https://example.com - * Text Domain: foo - * Domain Path: /languages - * Version: 0.1.0 - * - * @package Foo - */ - """ - - When I run `wp scaffold plugin-tests foo` - Then the wp-content/plugins/foo/tests/bootstrap.php file should contain: - """ - require dirname( __DIR__ ) . '/bar.php'; - """ - - Scenario: Accept bitbucket as valid CI in plugin scaffold - Given a WP install - When I run `wp plugin path` - Then save STDOUT as {PLUGIN_DIR} - - When I run `wp scaffold plugin hello-world --ci=bitbucket` - Then STDOUT should not be empty - And the {PLUGIN_DIR}/hello-world/.editorconfig file should exist - And the {PLUGIN_DIR}/hello-world/hello-world.php file should exist - And the {PLUGIN_DIR}/hello-world/readme.txt file should exist - And the {PLUGIN_DIR}/hello-world/bitbucket-pipelines.yml file should exist - And the {PLUGIN_DIR}/hello-world/tests directory should exist diff --git a/features/scaffold-theme-tests.feature b/features/scaffold-theme-tests.feature index d8756a5c7..287d95137 100644 --- a/features/scaffold-theme-tests.feature +++ b/features/scaffold-theme-tests.feature @@ -2,88 +2,93 @@ Feature: Scaffold theme unit tests Background: Given a WP install - And I try `wp theme install twentytwelve --force` - And I run `wp scaffold child-theme t12child --parent_theme=twentytwelve` + And I run `wp theme install p2` + And I run `wp scaffold child-theme p2child --parent_theme=p2` When I run `wp theme path` Then save STDOUT as {THEME_DIR} - @require-php-7.4 @require-mysql Scenario: Scaffold theme tests - When I run `wp scaffold theme-tests t12child` + When I run `wp scaffold theme-tests p2child` Then STDOUT should not be empty - And the {THEME_DIR}/t12child/tests directory should contain: + And the {THEME_DIR}/p2child/tests directory should contain: """ bootstrap.php test-sample.php """ - And the {THEME_DIR}/t12child/tests/bootstrap.php file should contain: + And the {THEME_DIR}/p2child/tests/bootstrap.php file should contain: """ register_theme_directory( $theme_root ); """ - And the {THEME_DIR}/t12child/tests/bootstrap.php file should contain: + And the {THEME_DIR}/p2child/tests/bootstrap.php file should contain: """ - * @package T12child + * @package P2child """ - And the {THEME_DIR}/t12child/tests/test-sample.php file should contain: + And the {THEME_DIR}/p2child/tests/test-sample.php file should contain: """ - * @package T12child + * @package P2child """ - And the {THEME_DIR}/t12child/bin directory should contain: + And the {THEME_DIR}/p2child/bin directory should contain: """ install-wp-tests.sh """ - And the {THEME_DIR}/t12child/phpunit.xml.dist file should contain: - """ - <exclude>./tests/test-sample.php</exclude> - """ - And the {THEME_DIR}/t12child/.phpcs.xml.dist file should exist - And the {THEME_DIR}/t12child/bitbucket-pipelines.yml file should not exist - And the {THEME_DIR}/t12child/.gitlab-ci.yml file should not exist - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: - """ - jobs: - php56-build: - <<: *php_job - docker: - - image: circleci/php:5.6 - - image: *mysql_image - """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: - """ - workflows: - version: 2 - main: - jobs: - - php56-build - - php70-build - - php71-build - - php72-build - - php73-build - - php74-build - """ - - When I run `wp eval "if ( is_executable( '{THEME_DIR}/t12child/bin/install-wp-tests.sh' ) ) { echo 'executable'; } else { exit( 1 ); }"` + And the {THEME_DIR}/p2child/phpunit.xml.dist file should exist + And the {THEME_DIR}/p2child/phpcs.xml.dist file should exist + And the {THEME_DIR}/p2child/circle.yml file should not exist + And the {THEME_DIR}/p2child/.circleci directory should not exist + And the {THEME_DIR}/p2child/.gitlab-ci.yml file should not exist + And the {THEME_DIR}/p2child/.travis.yml file should contain: + """ + script: + - | + if [[ ! -z "$WP_VERSION" ]] ; then + phpunit + WP_MULTISITE=1 phpunit + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + phpcs + fi + """ + And the {THEME_DIR}/p2child/.travis.yml file should contain: + """ + matrix: + include: + - 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=trunk + - php: 5.6 + env: WP_TRAVISCI=phpcs + - php: 5.3 + env: WP_VERSION=latest + """ + + When I run `wp eval "if ( is_executable( '{THEME_DIR}/p2child/bin/install-wp-tests.sh' ) ) { echo 'executable'; } else { exit( 1 ); }"` Then STDOUT should be: """ executable """ # Warning: overwriting generated functions.php file, so functions.php file loaded only tests beyond here... - Given a wp-content/themes/t12child/functions.php file: + Given a wp-content/themes/p2child/functions.php file: """ <?php echo __FILE__ . " loaded.\n"; """ - # This throws a warning for the password provided via command line. - And I try `mysql -u{DB_USER} -p{DB_PASSWORD} -h{MYSQL_HOST} -P{MYSQL_PORT} --protocol=tcp -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` - - And I try `WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib WP_CORE_DIR={RUN_DIR}/wordpress {THEME_DIR}/t12child/bin/install-wp-tests.sh wp_cli_test_scaffold {DB_USER} {DB_PASSWORD} {DB_HOST} latest` + And I run `MYSQL_PWD=password1 mysql -u wp_cli_test -e "DROP DATABASE IF EXISTS wp_cli_test_scaffold"` + And I try `rm -fr /tmp/behat-wordpress-tests-lib` + And I try `rm -fr /tmp/behat-wordpress` + And I try `WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib WP_CORE_DIR=/tmp/behat-wordpress {THEME_DIR}/p2child/bin/install-wp-tests.sh wp_cli_test_scaffold wp_cli_test password1 localhost latest` Then the return code should be 0 - When I run `cd {THEME_DIR}/t12child; WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib phpunit` + When I run `cd {THEME_DIR}/p2child; WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib phpunit` Then STDOUT should contain: """ - t12child/functions.php loaded. + p2child/functions.php loaded. """ And STDOUT should contain: """ @@ -91,13 +96,13 @@ Feature: Scaffold theme unit tests """ And STDOUT should contain: """ - No tests executed! + OK (1 test, 1 assertion) """ - When I run `cd {THEME_DIR}/t12child; WP_MULTISITE=1 WP_TESTS_DIR={RUN_DIR}/wordpress-tests-lib phpunit` + When I run `cd {THEME_DIR}/p2child; WP_MULTISITE=1 WP_TESTS_DIR=/tmp/behat-wordpress-tests-lib phpunit` Then STDOUT should contain: """ - t12child/functions.php loaded. + p2child/functions.php loaded. """ And STDOUT should contain: """ @@ -105,7 +110,7 @@ Feature: Scaffold theme unit tests """ And STDOUT should contain: """ - No tests executed! + OK (1 test, 1 assertion) """ Scenario: Scaffold theme tests invalid theme @@ -117,92 +122,36 @@ Feature: Scaffold theme unit tests And the return code should be 1 Scenario: Scaffold theme tests with Circle as the provider - When I run `wp scaffold theme-tests t12child --ci=circle` + When I run `wp scaffold theme-tests p2child --ci=circle` Then STDOUT should not be empty - And the {THEME_DIR}/t12child/circle.yml file should not exist - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: + And the {THEME_DIR}/p2child/.travis.yml file should not exist + And the {THEME_DIR}/p2child/circle.yml file should not exist + And the {THEME_DIR}/p2child/.circleci/config.yml file should contain: """ version: 2 """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: + And the {THEME_DIR}/p2child/.circleci/config.yml file should contain: """ php56-build """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: + And the {THEME_DIR}/p2child/.circleci/config.yml file should contain: """ php70-build """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: + And the {THEME_DIR}/p2child/.circleci/config.yml file should contain: """ php71-build """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: - """ - php72-build - """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: - """ - php73-build - """ - And the {THEME_DIR}/t12child/.circleci/config.yml file should contain: - """ - php74-build - """ Scenario: Scaffold theme tests with Gitlab as the provider - When I run `wp scaffold theme-tests t12child --ci=gitlab` + When I run `wp scaffold theme-tests p2child --ci=gitlab` Then STDOUT should not be empty - And the {THEME_DIR}/t12child/.gitlab-ci.yml file should contain: + And the {THEME_DIR}/p2child/.travis.yml file should not exist + And the {THEME_DIR}/p2child/.gitlab-ci.yml file should contain: """ MYSQL_DATABASE """ - Scenario: Scaffold theme tests with Bitbucket Pipelines as the provider - When I run `wp scaffold theme-tests t12child --ci=bitbucket` - Then STDOUT should not be empty - And the {THEME_DIR}/t12child/bitbucket-pipelines.yml file should contain: - """ - pipelines: - default: - """ - And the {THEME_DIR}/t12child/bitbucket-pipelines.yml file should contain: - """ - - step: - image: php:7.4 - name: "PHP 7.4" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - """ - And the {THEME_DIR}/t12child/bitbucket-pipelines.yml file should contain: - """ - - step: - image: php:8.0 - name: "PHP 8.0" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - """ - And the {THEME_DIR}/t12child/bitbucket-pipelines.yml file should contain: - """ - - step: - image: php:8.2 - name: "PHP 8.2" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - """ - And the {THEME_DIR}/t12child/bitbucket-pipelines.yml file should contain: - """ - definitions: - services: - database: - image: mysql:latest - environment: - MYSQL_DATABASE: 'wordpress_tests' - MYSQL_ROOT_PASSWORD: 'root' - """ - Scenario: Scaffold theme tests with invalid slug When I try `wp scaffold theme-tests .` @@ -215,26 +164,12 @@ Feature: Scaffold theme unit tests When I try `wp scaffold theme-tests ../` Then STDERR should be: """ - Error: Invalid theme slug specified. The slug can only contain alphanumeric characters, underscores, and dashes. - """ - And the return code should be 1 - - When I try `wp scaffold theme-tests t12child/` - Then STDERR should be: - """ - Error: Invalid theme slug specified. The slug can only contain alphanumeric characters, underscores, and dashes. - """ - And the return code should be 1 - - When I try `wp scaffold theme-tests t12child\\` - Then STDERR should be: - """ - Error: Invalid theme slug specified. The slug can only contain alphanumeric characters, underscores, and dashes. + Error: Invalid theme slug specified. The target directory '{RUN_DIR}/wp-content/themes/../' is not in '{RUN_DIR}/wp-content/themes'. """ And the return code should be 1 Scenario: Scaffold theme tests with invalid directory - When I try `wp scaffold theme-tests twentytwelve --dir=non-existent-dir` + When I try `wp scaffold theme-tests p2 --dir=non-existent-dir` Then STDERR should be: """ Error: Invalid theme directory specified. No such directory 'non-existent-dir'. @@ -242,30 +177,28 @@ Feature: Scaffold theme unit tests And the return code should be 1 # Temporarily move. - When I run `mv -f {THEME_DIR}/twentytwelve {THEME_DIR}/hide-twentytwelve && touch {THEME_DIR}/twentytwelve` + When I run `mv -f {THEME_DIR}/p2 {THEME_DIR}/hide-p2 && touch {THEME_DIR}/p2` Then the return code should be 0 - When I try `wp scaffold theme-tests twentytwelve` + When I try `wp scaffold theme-tests p2` Then STDERR should be: """ - Error: Invalid theme slug specified. No such target directory '{THEME_DIR}/twentytwelve'. + Error: Invalid theme slug specified. No such target directory '{THEME_DIR}/p2'. """ And the return code should be 1 # Restore. - When I run `rm -f {THEME_DIR}/twentytwelve && mv -f {THEME_DIR}/hide-twentytwelve {THEME_DIR}/twentytwelve` + When I run `rm -f {THEME_DIR}/p2 && mv -f {THEME_DIR}/hide-p2 {THEME_DIR}/p2` Then the return code should be 0 - # TODO: Fix this on Windows. Fails because unlink fails on directories. - @skip-windows Scenario: Scaffold theme tests with a symbolic link # Temporarily move the whole theme dir and create a symbolic link to it. When I run `mv -f {THEME_DIR} {RUN_DIR}/alt-themes && ln -s {RUN_DIR}/alt-themes {THEME_DIR}` Then the return code should be 0 - When I run `wp scaffold theme-tests twentytwelve` + When I run `wp scaffold theme-tests p2` Then STDOUT should not be empty - And the {THEME_DIR}/twentytwelve/tests directory should contain: + And the {THEME_DIR}/p2/tests directory should contain: """ bootstrap.php """ diff --git a/features/scaffold.feature b/features/scaffold.feature index 2cf2b0a5d..dad213125 100644 --- a/features/scaffold.feature +++ b/features/scaffold.feature @@ -3,16 +3,12 @@ Feature: WordPress code scaffolding @theme Scenario: Scaffold a child theme Given a WP install - And I run `wp theme path` + Given I run `wp theme path` And save STDOUT as {THEME_DIR} When I run `wp scaffold child-theme zombieland --parent_theme=umbrella --theme_name=Zombieland --author=Tallahassee --author_uri=https://wp-cli.org --theme_uri=http://www.zombieland.com` Then the {THEME_DIR}/zombieland/style.css file should exist And the {THEME_DIR}/zombieland/functions.php file should exist - And the {THEME_DIR}/zombieland/functions.php file should contain: - """ - wp_style_add_data( 'zombieland-style', 'rtl', 'add' ); - """ And STDOUT should be: """ Success: Created '{THEME_DIR}/zombieland'. @@ -21,7 +17,7 @@ Feature: WordPress code scaffolding Scenario: Scaffold a child theme with only --parent_theme parameter Given a WP install - And I run `wp theme path` + Given I run `wp theme path` And save STDOUT as {THEME_DIR} When I run `wp scaffold child-theme hello-world --parent_theme=simple-life` @@ -48,11 +44,10 @@ Feature: WordPress code scaffolding When I try `wp scaffold child-theme hello-world --parent_theme=just-test --enable-network --quiet` Then STDERR should contain: """ - Error: This is not a multisite install + Error: This is not a multisite install. """ And the return code should be 1 - @require-wp-4.6 Scenario: Scaffold a child theme and network enable it Given a WP multisite install @@ -62,44 +57,6 @@ Feature: WordPress code scaffolding Success: Network enabled the 'Zombieland' theme. """ - Scenario: Scaffold a child theme and activate it with different slug and name - Given a WP install - - When I run `wp theme install twentytwentyone --force` - Then STDOUT should not be empty - - And I run `wp theme path` - And save STDOUT as {THEME_DIR} - - When I run `wp scaffold child-theme first-run --parent_theme=twentytwentyone --theme_name="First Run Name" --activate` - Then STDOUT should contain: - """ - Success: Created '{THEME_DIR}/first-run'. - """ - And STDOUT should contain: - """ - Success: Switched to 'First Run Name' theme. - """ - - When I run `wp theme list --fields=name,status --format=csv` - Then STDOUT should contain: - """ - first-run,active - """ - - # Now delete the theme and create it again to test the fix for the caching issue - When I run `rm -rf {THEME_DIR}/first-run` - And I run `wp theme activate twentytwentyone` - And I run `wp scaffold child-theme first-run --parent_theme=twentytwentyone --theme_name="First Run Name" --activate` - Then STDOUT should contain: - """ - Success: Created '{THEME_DIR}/first-run'. - """ - And STDOUT should contain: - """ - Success: Switched to 'First Run Name' theme. - """ - Scenario: Scaffold a child theme with invalid slug Given a WP install When I try `wp scaffold child-theme . --parent_theme=simple-life` @@ -116,37 +73,10 @@ Feature: WordPress code scaffolding """ And the return code should be 1 - @theme - Scenario: Scaffold a child theme with a relative --path argument containing '..' - Given a WP install in 'subdir' - And I run `wp --path=.. theme path` from 'subdir/wp-content' - And save STDOUT as {THEME_DIR} - - When I run `wp scaffold child-theme zombieland --parent_theme=umbrella --path=..` from 'subdir/wp-content' - Then the {THEME_DIR}/zombieland/style.css file should exist - And the {THEME_DIR}/zombieland/functions.php file should exist - - @theme - Scenario: Scaffold a child theme with dots in the slug - Given a WP install - And I run `wp theme path` - And save STDOUT as {THEME_DIR} - - When I run `wp scaffold child-theme my-theme-2.0.1 --parent_theme=umbrella` - Then the {THEME_DIR}/my-theme-2.0.1/functions.php file should exist - And the {THEME_DIR}/my-theme-2.0.1/functions.php file should contain: - """ - function my_theme_2_0_1_parent_theme_enqueue_styles() - """ - And the {THEME_DIR}/my-theme-2.0.1/functions.php file should contain: - """ - add_action( 'wp_enqueue_scripts', 'my_theme_2_0_1_parent_theme_enqueue_styles' ); - """ - @tax @cpt Scenario: Scaffold a Custom Taxonomy and Custom Post Type and write it to active theme Given a WP install - And I run `wp eval 'echo STYLESHEETPATH;'` + Given I run `wp eval 'echo STYLESHEETPATH;'` And save STDOUT as {STYLESHEETPATH} When I run `wp scaffold taxonomy zombie-speed --theme` @@ -162,7 +92,7 @@ Feature: WordPress code scaffolding When I run `wp scaffold post-type zombie` Then STDOUT should contain: """ - 'rest_base' => 'zombie' + register_post_type( 'zombie' """ And STDOUT should contain: """ @@ -197,13 +127,13 @@ Feature: WordPress code scaffolding Given a WP install When I run `wp scaffold taxonomy zombie-speed --label="Speed"` Then STDOUT should contain: - """ - __( 'Speeds' - """ + """ + __( 'Speeds' + """ And STDOUT should contain: - """ - _x( 'Speed', 'taxonomy general name', - """ + """ + _x( 'Speed', 'taxonomy general name', + """ # Test for all flags but --label, --theme, --plugin and --raw @cpt @@ -267,9 +197,9 @@ Feature: WordPress code scaffolding Scenario: Scaffold a plugin Given a WP install - And I run `wp plugin path` + Given I run `wp plugin path` And save STDOUT as {PLUGIN_DIR} - And I run `wp core version` + Given I run `wp core version` And save STDOUT as {WP_VERSION} When I run `wp scaffold plugin hello-world --plugin_author="Hello World Author"` @@ -278,7 +208,8 @@ Feature: WordPress code scaffolding And the {PLUGIN_DIR}/hello-world/.editorconfig file should exist And the {PLUGIN_DIR}/hello-world/hello-world.php file should exist And the {PLUGIN_DIR}/hello-world/readme.txt file should exist - And the {PLUGIN_DIR}/hello-world/composer.json file should exist + And the {PLUGIN_DIR}/hello-world/package.json file should exist + And the {PLUGIN_DIR}/hello-world/Gruntfile.js file should exist And the {PLUGIN_DIR}/hello-world/.gitignore file should contain: """ .DS_Store @@ -287,21 +218,12 @@ Feature: WordPress code scaffolding Thumbs.db wp-cli.local.yml node_modules/ - vendor/ """ And the {PLUGIN_DIR}/hello-world/.distignore file should contain: """ .git .gitignore """ - And the {PLUGIN_DIR}/hello-world/.phpcs.xml.dist file should contain: - """ - <rule ref="PHPCompatibilityWP"/> - """ - And the {PLUGIN_DIR}/hello-world/.phpcs.xml.dist file should contain: - """ - <config name="testVersion" value="7.2-"/> - """ And the {PLUGIN_DIR}/hello-world/hello-world.php file should contain: """ * Plugin Name: Hello World @@ -323,10 +245,14 @@ Feature: WordPress code scaffolding Tested up to: {WP_VERSION} """ - When I run `cat {PLUGIN_DIR}/hello-world/composer.json` - Then STDOUT should contain: + When I run `cat {PLUGIN_DIR}/hello-world/package.json` + Then STDOUT should be JSON containing: + """ + {"author":"Hello World Author"} """ - wp-cli/i18n-command + And STDOUT should be JSON containing: + """ + {"version":"0.1.0"} """ Scenario: Scaffold a plugin by prompting @@ -341,7 +267,7 @@ Feature: WordPress code scaffolding https://wp-cli.org https://wp-cli.org n - circle + travis Y n n @@ -375,7 +301,6 @@ Feature: WordPress code scaffolding Plugin 'hello-world' activated. """ - @require-wp-4.6 Scenario: Scaffold a plugin and network activate it Given a WP multisite install When I run `wp scaffold plugin hello-world --activate-network` @@ -400,14 +325,12 @@ Feature: WordPress code scaffolding """ And the return code should be 1 - @require-php-5.6 @require-wp-4.6 Scenario: Scaffold starter code for a theme Given a WP install - And I run `wp theme path` + Given I run `wp theme path` And save STDOUT as {THEME_DIR} - # Allow for warnings to be generated due to https://github.com/wp-cli/scaffold-command/issues/181 - When I try `wp scaffold _s starter-theme` + When I run `wp scaffold _s starter-theme` Then STDOUT should contain: """ Success: Created theme 'Starter-theme'. @@ -415,40 +338,21 @@ Feature: WordPress code scaffolding And the {THEME_DIR}/starter-theme/style.css file should exist And the {THEME_DIR}/starter-theme/.editorconfig file should exist - @require-php-5.6 @require-wp-4.6 Scenario: Scaffold starter code for a theme with sass Given a WP install - And I run `wp theme path` + Given I run `wp theme path` And save STDOUT as {THEME_DIR} - # Allow for warnings to be generated due to https://github.com/wp-cli/scaffold-command/issues/181 - When I try `wp scaffold _s starter-theme --sassify` + When I run `wp scaffold _s starter-theme --sassify` Then STDOUT should contain: """ Success: Created theme 'Starter-theme'. """ And the {THEME_DIR}/starter-theme/sass directory should exist - @require-php-5.6 @require-wp-4.6 - Scenario: Scaffold starter code for a WooCommerce theme - Given a WP install - And I run `wp theme path` - And save STDOUT as {THEME_DIR} - - # Allow for warnings to be generated due to https://github.com/wp-cli/scaffold-command/issues/181 - When I try `wp scaffold _s starter-theme --woocommerce` - Then STDOUT should contain: - """ - Success: Created theme 'Starter-theme'. - """ - And the {THEME_DIR}/starter-theme/woocommerce.css file should exist - And the {THEME_DIR}/starter-theme/inc/woocommerce.php file should exist - - @require-php-5.6 @require-wp-4.6 @require-mysql Scenario: Scaffold starter code for a theme and activate it Given a WP install - # Allow for warnings to be generated due to https://github.com/wp-cli/scaffold-command/issues/181 - When I try `wp scaffold _s starter-theme --activate` + When I run `wp scaffold _s starter-theme --activate` Then STDOUT should contain: """ Success: Switched to 'Starter-theme' theme. @@ -500,9 +404,9 @@ Feature: WordPress code scaffolding And the wp-content/mu-plugins/custom-plugin/tests directory should exist And the wp-content/mu-plugins/custom-plugin/tests/bootstrap.php file should exist And the wp-content/mu-plugins/custom-plugin/tests/bootstrap.php file should contain: - """ - require dirname( __DIR__ ) . '/custom-plugin.php'; - """ + """ + require dirname( dirname( __FILE__ ) ) . '/custom-plugin.php'; + """ Scenario: Scaffold tests for a plugin with a different slug than plugin directory Given a WP install @@ -524,9 +428,9 @@ Feature: WordPress code scaffolding And the wp-content/mu-plugins/custom-plugin2/tests directory should exist And the wp-content/mu-plugins/custom-plugin2/tests/bootstrap.php file should exist And the wp-content/mu-plugins/custom-plugin2/tests/bootstrap.php file should contain: - """ - require dirname( __DIR__ ) . '/custom-plugin-slug.php'; - """ + """ + require dirname( dirname( __FILE__ ) ) . '/custom-plugin-slug.php'; + """ Scenario: Scaffold tests parses plugin readme.txt Given a WP install @@ -538,40 +442,43 @@ Feature: WordPress code scaffolding When I run `wp scaffold plugin hello-world` Then STDOUT should not be empty And the {PLUGIN_DIR}/hello-world/readme.txt file should exist - And the {PLUGIN_DIR}/hello-world/.circleci/config.yml file should exist - And the {PLUGIN_DIR}/hello-world/.circleci/config.yml file should contain: - """ - workflows: - version: 2 - main: - jobs: - - php56-build - - php70-build - - php71-build - - php72-build - - php73-build - - php74-build - """ - - @require-php-5.6 @require-wp-4.6 + And the {PLUGIN_DIR}/hello-world/.travis.yml file should exist + And the {PLUGIN_DIR}/hello-world/.travis.yml file should contain: + """ + matrix: + include: + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=4.5 + - php: 5.6 + env: WP_VERSION=latest + - php: 5.6 + env: WP_VERSION=trunk + - php: 5.6 + env: WP_TRAVISCI=phpcs + - php: 5.3 + env: WP_VERSION=latest + """ + Scenario: Scaffold starter code for a theme and network enable it Given a WP multisite install - # Allow for warnings to be generated due to https://github.com/wp-cli/scaffold-command/issues/181 - When I try `wp scaffold _s starter-theme --enable-network` + When I run `wp scaffold _s starter-theme --enable-network` Then STDOUT should contain: """ Success: Network enabled the 'Starter-theme' theme. """ - @require-php-5.6 @require-wp-4.6 @require-mysql Scenario: Scaffold starter code for a theme, but can't unzip theme files Given a WP install And a misconfigured WP_CONTENT_DIR constant directory When I try `wp scaffold _s starter-theme` Then STDERR should contain: - """ - Error: Could not decompress your theme files - """ + """ + Error: Could not decompress your theme files + """ And the return code should be 1 Scenario: Overwrite existing files @@ -579,13 +486,13 @@ Feature: WordPress code scaffolding When I run `wp scaffold plugin test` And I try `wp scaffold plugin test --force` Then STDERR should contain: - """ - already exists - """ + """ + already exists + """ And STDOUT should contain: - """ - Replacing - """ + """ + Replacing + """ And the return code should be 0 Scenario: Scaffold tests for invalid plugin directory diff --git a/features/steps/given.php b/features/steps/given.php new file mode 100644 index 000000000..3e501c2c7 --- /dev/null +++ b/features/steps/given.php @@ -0,0 +1,219 @@ +<?php + +use Behat\Gherkin\Node\PyStringNode, + Behat\Gherkin\Node\TableNode, + WP_CLI\Process; + +$steps->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 @@ +<?php + +use Behat\Gherkin\Node\PyStringNode, + Behat\Gherkin\Node\TableNode; + +$steps->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 @@ +<?php + +use Behat\Gherkin\Node\PyStringNode, + Behat\Gherkin\Node\TableNode, + WP_CLI\Process; + +function invoke_proc( $proc, $mode ) { + $map = array( + 'run' => '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/phpcs.xml.dist b/phpcs.xml.dist deleted file mode 100644 index 5e9e44b3b..000000000 --- a/phpcs.xml.dist +++ /dev/null @@ -1,59 +0,0 @@ -<?xml version="1.0"?> -<ruleset name="WP-CLI-scaffold"> - <description>Custom ruleset for WP-CLI scaffold-command</description> - - <!-- - ############################################################################# - COMMAND LINE ARGUMENTS - For help understanding this file: https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Annotated-ruleset.xml - For help using PHPCS: https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Usage - ############################################################################# - --> - - <!-- What to scan. --> - <file>.</file> - - <!-- Show progress. --> - <arg value="p"/> - - <!-- Strip the filepaths down to the relevant bit. --> - <arg name="basepath" value="./"/> - - <!-- Check up to 8 files simultaneously. --> - <arg name="parallel" value="8"/> - - <!-- - ############################################################################# - USE THE WP_CLI_CS RULESET - ############################################################################# - --> - - <rule ref="WP_CLI_CS"/> - - <!-- - ############################################################################# - PROJECT SPECIFIC CONFIGURATION FOR SNIFFS - ############################################################################# - --> - - <!-- For help understanding the `testVersion` configuration setting: - https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions --> - <config name="testVersion" value="7.2-"/> - - <!-- Verify that everything in the global namespace is either namespaced or prefixed. - See: https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#naming-conventions-prefix-everything-in-the-global-namespace --> - <rule ref="WordPress.NamingConventions.PrefixAllGlobals"> - <properties> - <property name="prefixes" type="array"> - <element value="WP_CLI\Scaffold"/><!-- Namespaces. --> - <element value="wpcli_scaffold"/><!-- Global variables and such. --> - </property> - </properties> - </rule> - - <!-- Exclude existing classes from the prefix rule as it would break BC to prefix them now. --> - <rule ref="WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound"> - <exclude-pattern>*/src/Scaffold_Command\.php$</exclude-pattern> - </rule> - -</ruleset> diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index 20dfba73b..000000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,13 +0,0 @@ -parameters: - level: 9 - paths: - - src - - scaffold-command.php - scanDirectories: - - vendor/wp-cli/wp-cli/php - scanFiles: - - vendor/php-stubs/wordpress-stubs/wordpress-stubs.php - treatPhpDocTypesAsCertain: false - ignoreErrors: - - identifier: missingType.parameter - - identifier: missingType.return diff --git a/scaffold-command.php b/scaffold-command.php index a2b0f5010..9930bc76d 100644 --- a/scaffold-command.php +++ b/scaffold-command.php @@ -4,9 +4,9 @@ return; } -$wpcli_scaffold_autoloader = __DIR__ . '/vendor/autoload.php'; -if ( file_exists( $wpcli_scaffold_autoloader ) ) { - require_once $wpcli_scaffold_autoloader; +$autoload = dirname( __FILE__ ) . '/vendor/autoload.php'; +if ( file_exists( $autoload ) ) { + require_once $autoload; } WP_CLI::add_command( 'scaffold', 'Scaffold_Command' ); diff --git a/src/Scaffold_Command.php b/src/Scaffold_Command.php index f1a3e6beb..d8e6257ff 100644 --- a/src/Scaffold_Command.php +++ b/src/Scaffold_Command.php @@ -1,7 +1,7 @@ <?php use WP_CLI\Utils; -use WP_CLI\Path; +use WP_CLI\Process; use WP_CLI\Inflector; /** @@ -9,18 +9,18 @@ * * ## EXAMPLES * - * # Generate a new plugin with unit tests. + * # Generate a new plugin with unit tests * $ wp scaffold plugin sample-plugin * Success: Created plugin files. * Success: Created test files. * - * # Generate theme based on _s. + * # Generate theme based on _s * $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe" * Success: Created theme 'Sample Theme'. * - * # Generate code for post type registration in given theme. + * # Generate code for post type registration in given theme * $ wp scaffold post-type movie --label=Movie --theme=simple-life - * Success: Created '/var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php'. + * Success: Created /var/www/example.com/public_html/wp-content/themes/simple-life/post-types/movie.php * * @package wp-cli */ @@ -69,20 +69,18 @@ class Scaffold_Command extends WP_CLI_Command { public function post_type( $args, $assoc_args ) { if ( strlen( $args[0] ) > 20 ) { - WP_CLI::error( 'Post type slugs cannot exceed 20 characters in length.' ); + WP_CLI::error( "Post type slugs cannot exceed 20 characters in length." ); } - $defaults = [ + $defaults = array( 'textdomain' => '', 'dashicon' => 'admin-post', - ]; + ); - $templates = [ + $this->_scaffold( $args[0], $assoc_args, $defaults, '/post-types/', array( 'post_type.mustache', 'post_type_extended.mustache', - ]; - - $this->scaffold( $args[0], $assoc_args, $defaults, '/post-types/', $templates ); + ) ); } /** @@ -125,33 +123,30 @@ public function post_type( $args, $assoc_args ) { * @alias tax */ public function taxonomy( $args, $assoc_args ) { - $defaults = [ + $defaults = array( 'textdomain' => '', 'post_types' => "'post'", - ]; + ); if ( isset( $assoc_args['post_types'] ) ) { $assoc_args['post_types'] = $this->quote_comma_list_elements( $assoc_args['post_types'] ); } - $templates = [ + $this->_scaffold( $args[0], $assoc_args, $defaults, '/taxonomies/', array( 'taxonomy.mustache', - 'taxonomy_extended.mustache', - ]; - - $this->scaffold( $args[0], $assoc_args, $defaults, '/taxonomies/', $templates ); + 'taxonomy_extended.mustache' + ) ); } - private function scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) { + private function _scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) { $wp_filesystem = $this->init_wp_filesystem(); - $control_defaults = [ + $control_args = $this->extract_args( $assoc_args, array( 'label' => preg_replace( '/_|-/', ' ', strtolower( $slug ) ), 'theme' => false, 'plugin' => false, 'raw' => false, - ]; - $control_args = $this->extract_args( $assoc_args, $control_defaults ); + ) ); $vars = $this->extract_args( $assoc_args, $defaults ); @@ -178,41 +173,26 @@ private function scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) $raw_output = self::mustache_render( $raw_template, $vars ); if ( ! $control_args['raw'] ) { - $vars['machine_name'] = $machine_name; - $vars['output'] = rtrim( $raw_output ); - - $target_slug = ''; - - if ( true === $control_args['theme'] ) { - $target_slug = get_stylesheet(); - } elseif ( false !== $control_args['theme'] ) { - $target_slug = $control_args['theme']; - } elseif ( false !== $control_args['plugin'] ) { - $target_slug = $control_args['plugin']; - } - - $target_name = ( $target_slug ) ? $this->generate_machine_name( $target_slug ) : ''; - - if ( empty( $target_name ) ) { - $target_name = $machine_name; - } - - $vars['prefix'] = $target_name; + $vars = array_merge( $vars, array( + 'machine_name' => $machine_name, + 'output' => $raw_output, + ) ); $final_output = self::mustache_render( $extended_template, $vars ); } else { $final_output = $raw_output; } - $path = $this->get_output_path( $control_args, $subdir ); - if ( is_string( $path ) && ! empty( $path ) ) { - $filename = "{$path}{$slug}.php"; + if ( $path = $this->get_output_path( $control_args, $subdir ) ) { + $filename = $path . $slug . '.php'; - $force = Utils\get_flag_value( $assoc_args, 'force' ); - $files_written = $this->create_files( [ $filename => $final_output ], $force ); - $skip_message = "Skipped creating '{$filename}'."; - $success_message = "Created '{$filename}'."; - $this->log_whether_files_written( $files_written, $skip_message, $success_message ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); + $files_written = $this->create_files( array( $filename => $final_output ), $force ); + $this->log_whether_files_written( + $files_written, + $skip_message = "Skipped creating '$filename'.", + $success_message = "Created '$filename'." + ); } else { // STDOUT @@ -223,11 +203,11 @@ private function scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) /** * Generates PHP, JS and CSS code for registering a Gutenberg block for a plugin or theme. * - * **Warning: `wp scaffold block` is deprecated.** + * Blocks are the fundamental element of the Gutenberg editor. They are the primary way in which plugins and themes can register their own functionality and extend the capabilities of the editor. * - * The official script to generate a block is the [@wordpress/create-block](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package. + * Visit the [Gutenberg handbook](https://wordpress.org/gutenberg/handbook/block-api/) to learn more about Block API. * - * See the [Create a Block tutorial](https://developer.wordpress.org/block-editor/getting-started/tutorial/) for a complete walk-through. + * When you scaffold a block you must use either the theme or plugin option. The latter is recommended. * * ## OPTIONS * @@ -261,63 +241,72 @@ private function scaffold( $slug, $assoc_args, $defaults, $subdir, $templates ) * [--force] * : Overwrite files that already exist. * + * ## EXAMPLES + * + * # Generate a 'movie' block for the 'movies' plugin + * $ wp scaffold block movie --title="Movie block" --plugin=movies + * Success: Created block 'Movie block'. + * + * # Generate a 'movie' block for the 'simple-life' theme + * $ wp scaffold block movie --title="Movie block" --theme=simple-life + * Success: Created block 'Movie block'. + * + * # Create a new plugin and add two blocks + * # Create plugin called books + * $ wp scaffold plugin books + * # Add a block called book to plugin books + * $ wp scaffold block book --title="Book" --plugin=books + * # Add a second block to plugin called books. + * $ wp scaffold block books --title="Book List" --plugin=books + * * @subcommand block */ public function block( $args, $assoc_args ) { $slug = $args[0]; if ( ! preg_match( '/^[a-z][a-z0-9\-]*$/', $slug ) ) { - WP_CLI::error( 'Invalid block slug specified. Block slugs can contain only lowercase alphanumeric characters or dashes, and start with a letter.' ); + WP_CLI::error( "Invalid block slug specified. Block slugs can contain only lowercase alphanumeric characters or dashes, and start with a letter." ); } - $defaults = [ - 'title' => str_replace( '-', ' ', $slug ), - 'category' => 'widgets', - ]; + $defaults = array( + 'title' => str_replace( '-', ' ', $slug ), + 'category' => 'widgets', + ); $data = $this->extract_args( $assoc_args, $defaults ); - $data['slug'] = $slug; - $data['title_ucfirst'] = ucfirst( $data['title'] ); - $data['title_ucfirst_js'] = esc_js( $data['title_ucfirst'] ); + $data['slug'] = $slug; + $data['title_ucfirst'] = ucfirst( $data['title'] ); $dashicon = $this->extract_dashicon( $assoc_args ); if ( $dashicon ) { $data['dashicon'] = $dashicon; } - $control_defaults = [ + $control_args = $this->extract_args( $assoc_args, array( 'force' => false, 'plugin' => false, 'theme' => false, - ]; - $control_args = $this->extract_args( $assoc_args, $control_defaults ); - - if ( isset( $control_args['plugin'] ) ) { - if ( ! preg_match( '/^[A-Za-z0-9\-]*$/', $control_args['plugin'] ) ) { - WP_CLI::error( 'Invalid plugin name specified. The block editor can only register blocks for plugins that have nothing but lowercase alphanumeric characters or dashes in their slug.' ); - } - } + ) ); - $data['namespace'] = $control_args['plugin'] ? $control_args['plugin'] : $this->get_theme_name( $control_args['theme'] ); + $data['namespace'] = $control_args['plugin'] ? $control_args['plugin'] : $this->get_theme_name( $control_args['theme'] ); $data['machine_name'] = $this->generate_machine_name( $slug ); - $data['plugin'] = $control_args['plugin'] ? true : false; - $data['theme'] = ! $data['plugin']; - $block_dir = $this->get_output_path( $control_args, '/blocks' ); + $block_dir = $this->get_output_path( $control_args, "/blocks" ); if ( ! $block_dir ) { - WP_CLI::error( 'No plugin or theme selected.' ); + WP_CLI::error( "No plugin or theme selected." ); } - $files_to_create = [ - "{$block_dir}/{$slug}.php" => self::mustache_render( 'block-php.mustache', $data ), - "{$block_dir}/{$slug}/index.js" => self::mustache_render( 'block-index-js.mustache', $data ), - "{$block_dir}/{$slug}/editor.css" => self::mustache_render( 'block-editor-css.mustache', $data ), - "{$block_dir}/{$slug}/style.css" => self::mustache_render( 'block-style-css.mustache', $data ), - ]; - $files_written = $this->create_files( $files_to_create, $control_args['force'] ); - $skip_message = 'All block files were skipped.'; - $success_message = "Created block '{$data['title_ucfirst']}'."; - $this->log_whether_files_written( $files_written, $skip_message, $success_message ); + $files_written = $this->create_files( array( + "$block_dir/$slug.php" => self::mustache_render( 'block-php.mustache', $data ), + "$block_dir/$slug/block.js" => self::mustache_render( 'block-block-js.mustache', $data ), + "$block_dir/$slug/editor.css" => self::mustache_render( 'block-editor-css.mustache', $data ), + "$block_dir/$slug/style.css" => self::mustache_render( 'block-style-css.mustache', $data ), + ), $control_args['force'] ); + $this->log_whether_files_written( + $files_written, + $skip_message = 'All block files were skipped.', + $success_message = "Created block '{$data['title_ucfirst']}'." + ); } /** @@ -348,9 +337,6 @@ public function block( $args, $assoc_args ) { * [--sassify] * : Include stylesheets as SASS. * - * [--woocommerce] - * : Include WooCommerce boilerplate files. - * * [--force] * : Overwrite files that already exist. * @@ -359,76 +345,66 @@ public function block( $args, $assoc_args ) { * # Generate a theme with name "Sample Theme" and author "John Doe" * $ wp scaffold _s sample-theme --theme_name="Sample Theme" --author="John Doe" * Success: Created theme 'Sample Theme'. - * - * @alias _s */ - public function underscores( $args, $assoc_args ) { + public function _s( $args, $assoc_args ) { $theme_slug = $args[0]; - $theme_path = WP_CONTENT_DIR . '/themes'; - $url = 'https://underscores.me'; + $theme_path = WP_CONTENT_DIR . "/themes"; + $url = "https://underscores.me"; $timeout = 30; if ( ! preg_match( '/^[a-z_]\w+$/i', str_replace( '-', '_', $theme_slug ) ) ) { - WP_CLI::error( 'Invalid theme slug specified. Theme slugs can only contain letters, numbers, underscores and hyphens, and can only start with a letter or underscore.' ); + WP_CLI::error( "Invalid theme slug specified. Theme slugs can only contain letters, numbers, underscores and hyphens, and can only start with a letter or underscore." ); } - $defaults = [ + $data = wp_parse_args( $assoc_args, array( 'theme_name' => ucfirst( $theme_slug ), - 'author' => 'Me', - 'author_uri' => '', - ]; - $data = wp_parse_args( $assoc_args, $defaults ); + 'author' => "Me", + 'author_uri' => "", + ) ); $_s_theme_path = "$theme_path/$data[theme_name]"; - $error_msg = $this->check_target_directory( 'theme', $_s_theme_path ); - if ( ! empty( $error_msg ) ) { + if ( $error_msg = $this->check_target_directory( "theme", $_s_theme_path ) ) { WP_CLI::error( "Invalid theme slug specified. {$error_msg}" ); } - $force = Utils\get_flag_value( $assoc_args, 'force' ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); $should_write_file = $this->prompt_if_files_will_be_overwritten( $_s_theme_path, $force ); if ( ! $should_write_file ) { WP_CLI::log( 'No files created' ); die; } - $theme_description = "Custom theme: {$data['theme_name']}, developed by {$data['author']}"; + $theme_description = "Custom theme: " . $data['theme_name'] . ", developed by " . $data['author']; - $body = []; + $body = array(); $body['underscoresme_name'] = $data['theme_name']; $body['underscoresme_slug'] = $theme_slug; $body['underscoresme_author'] = $data['author']; $body['underscoresme_author_uri'] = $data['author_uri']; $body['underscoresme_description'] = $theme_description; - $body['underscoresme_generate_submit'] = 'Generate'; - $body['underscoresme_generate'] = '1'; - if ( Utils\get_flag_value( $assoc_args, 'sassify' ) ) { + $body['underscoresme_generate_submit'] = "Generate"; + $body['underscoresme_generate'] = "1"; + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'sassify' ) ) { $body['underscoresme_sass'] = 1; } - if ( Utils\get_flag_value( $assoc_args, 'woocommerce' ) ) { - $body['underscoresme_woocommerce'] = 1; - } - - $tmpfname = wp_tempnam( $url ); - $post_args = [ + $tmpfname = wp_tempnam( $url ); + $response = wp_remote_post( $url, array( 'timeout' => $timeout, 'body' => $body, 'stream' => true, 'filename' => $tmpfname, - ]; - - $response = wp_remote_post( $url, $post_args ); + ) ); if ( is_wp_error( $response ) ) { WP_CLI::error( $response ); } $response_code = wp_remote_retrieve_response_code( $response ); - if ( 200 !== (int) $response_code ) { - WP_CLI::error( "Couldn't create theme (received {$response_code} response)." ); + if ( 200 != $response_code ) { + WP_CLI::error( "Couldn't create theme (received $response_code response)." ); } $this->maybe_create_themes_dir(); @@ -439,19 +415,18 @@ public function underscores( $args, $assoc_args ) { unlink( $tmpfname ); if ( true === $unzip_result ) { - $files_to_create = [ - "{$theme_path}/{$theme_slug}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), - ]; - $this->create_files( $files_to_create, false ); + $this->create_files( array( + "$theme_path/{$theme_slug}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), + ), false ); WP_CLI::success( "Created theme '{$data['theme_name']}'." ); } else { WP_CLI::error( "Could not decompress your theme files ('{$tmpfname}') at '{$theme_path}': {$unzip_result->get_error_message()}" ); } - if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { - WP_CLI::run_command( [ 'theme', 'activate', $theme_slug ] ); - } elseif ( Utils\get_flag_value( $assoc_args, 'enable-network' ) ) { - WP_CLI::run_command( [ 'theme', 'enable', $theme_slug ], [ 'network' => true ] ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'activate' ) ) { + WP_CLI::run_command( array( 'theme', 'activate', $theme_slug ) ); + } else if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'enable-network' ) ) { + WP_CLI::run_command( array( 'theme', 'enable', $theme_slug ), array( 'network' => true ) ); } } @@ -497,54 +472,50 @@ public function underscores( $args, $assoc_args ) { * * @subcommand child-theme */ - public function child_theme( $args, $assoc_args ) { + function child_theme( $args, $assoc_args ) { $theme_slug = $args[0]; - if ( in_array( $theme_slug, [ '.', '..' ], true ) ) { + if ( in_array( $theme_slug, array( '.', '..' ) ) ) { WP_CLI::error( "Invalid theme slug specified. The slug cannot be '.' or '..'." ); } - $defaults = [ + $data = wp_parse_args( $assoc_args, array( 'theme_name' => ucfirst( $theme_slug ), - 'author' => 'Me', - 'author_uri' => '', - 'theme_uri' => '', - ]; + 'author' => "Me", + 'author_uri' => "", + 'theme_uri' => "", + ) ); + $data['slug'] = $theme_slug; + $data['parent_theme_function_safe'] = str_replace( array( ' ', '-' ), '_', $data['parent_theme'] ); + $data['description'] = ucfirst( $data['parent_theme'] ) . " child theme."; - $data = wp_parse_args( $assoc_args, $defaults ); - $data['slug'] = $theme_slug; - $data['prefix_safe'] = preg_replace( '/[^a-zA-Z0-9_]/', '_', $theme_slug ); - $data['description'] = ucfirst( $data['parent_theme'] ) . ' child theme.'; + $theme_dir = WP_CONTENT_DIR . "/themes" . "/$theme_slug"; - $theme_dir = WP_CONTENT_DIR . "/themes/{$theme_slug}"; - - $error_msg = $this->check_target_directory( 'theme', $theme_dir ); - if ( ! empty( $error_msg ) ) { + if ( $error_msg = $this->check_target_directory( "theme", $theme_dir ) ) { WP_CLI::error( "Invalid theme slug specified. {$error_msg}" ); } - $theme_style_path = "{$theme_dir}/style.css"; - $theme_functions_path = "{$theme_dir}/functions.php"; + $theme_style_path = "$theme_dir/style.css"; + $theme_functions_path = "$theme_dir/functions.php"; $this->maybe_create_themes_dir(); - $files_to_create = [ - $theme_style_path => self::mustache_render( 'child_theme.mustache', $data ), - $theme_functions_path => self::mustache_render( 'child_theme_functions.mustache', $data ), - "{$theme_dir}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), - ]; - $force = Utils\get_flag_value( $assoc_args, 'force' ); - $files_written = $this->create_files( $files_to_create, $force ); - $skip_message = 'All theme files were skipped.'; - $success_message = "Created '{$theme_dir}'."; - $this->log_whether_files_written( $files_written, $skip_message, $success_message ); - - if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { - wp_get_theme( $theme_slug )->cache_delete(); - WP_CLI::run_command( [ 'theme', 'activate', $theme_slug ] ); - } elseif ( Utils\get_flag_value( $assoc_args, 'enable-network' ) ) { - wp_get_theme( $theme_slug )->cache_delete(); - WP_CLI::run_command( [ 'theme', 'enable', $theme_slug ], [ 'network' => true ] ); + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); + $files_written = $this->create_files( array( + $theme_style_path => self::mustache_render( 'child_theme.mustache', $data ), + $theme_functions_path => self::mustache_render( 'child_theme_functions.mustache', $data ), + "$theme_dir/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), + ), $force ); + $this->log_whether_files_written( + $files_written, + $skip_message = 'All theme files were skipped.', + $success_message = "Created '$theme_dir'." + ); + + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'activate' ) ) { + WP_CLI::run_command( array( 'theme', 'activate', $theme_slug ) ); + } else if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'enable-network' ) ) { + WP_CLI::run_command( array( 'theme', 'enable', $theme_slug ), array( 'network' => true ) ); } } @@ -552,18 +523,18 @@ private function get_output_path( $assoc_args, $subdir ) { if ( $assoc_args['theme'] ) { $theme = $assoc_args['theme']; if ( is_string( $theme ) ) { - $path = get_theme_root( $theme ) . "/{$theme}"; + $path = get_theme_root( $theme ) . '/' . $theme; } else { $path = get_stylesheet_directory(); } if ( ! is_dir( $path ) ) { - WP_CLI::error( "Can't find '{$theme}' theme." ); + WP_CLI::error( "Can't find '$theme' theme." ); } } elseif ( $assoc_args['plugin'] ) { $plugin = $assoc_args['plugin']; - $path = WP_PLUGIN_DIR . "/{$plugin}"; + $path = WP_PLUGIN_DIR . '/' . $plugin; if ( ! is_dir( $path ) ) { - WP_CLI::error( "Can't find '{$plugin}' plugin." ); + WP_CLI::error( "Can't find '$plugin' plugin." ); } } else { return false; @@ -581,7 +552,7 @@ private function get_output_path( $assoc_args, $subdir ) { * * * `plugin-slug.php` is the main PHP plugin file. * * `readme.txt` is the readme file for the plugin. - * * `package.json` needed by NPM holds various metadata relevant to the project. Packages: `grunt`, `grunt-wp-i18n` and `grunt-wp-readme-to-markdown`. Scripts: `start`, `readme`, `i18n`. + * * `package.json` needed by NPM holds various metadata relevant to the project. Packages: `grunt`, `grunt-wp-i18n` and `grunt-wp-readme-to-markdown`. * * `Gruntfile.js` is the JS file containing Grunt tasks. Tasks: `i18n` containing `addtextdomain` and `makepot`, `readme` containing `wp_readme_to_markdown`. * * `.editorconfig` is the configuration file for Editor. * * `.gitignore` tells which files (or patterns) git should ignore. @@ -590,11 +561,11 @@ private function get_output_path( $assoc_args, $subdir ) { * The following files are also included unless the `--skip-tests` is used: * * * `phpunit.xml.dist` is the configuration file for PHPUnit. - * * `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service. + * * `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite. * * `tests/test-sample.php` is a sample file containing test cases. - * * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. + * * `phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. * * ## OPTIONS * @@ -625,12 +596,11 @@ private function get_output_path( $assoc_args, $subdir ) { * [--ci=<provider>] * : Choose a configuration file for a continuous integration provider. * --- - * default: circle + * default: travis * options: + * - travis * - circle * - gitlab - * - bitbucket - * - github * --- * * [--activate] @@ -648,16 +618,16 @@ private function get_output_path( $assoc_args, $subdir ) { * Success: Created plugin files. * Success: Created test files. */ - public function plugin( $args, $assoc_args ) { + function plugin( $args, $assoc_args ) { $plugin_slug = $args[0]; $plugin_name = ucwords( str_replace( '-', ' ', $plugin_slug ) ); $plugin_package = str_replace( ' ', '_', $plugin_name ); - if ( in_array( $plugin_slug, [ '.', '..' ], true ) ) { + if ( in_array( $plugin_slug, array( '.', '..' ) ) ) { WP_CLI::error( "Invalid plugin slug specified. The slug cannot be '.' or '..'." ); } - $defaults = [ + $data = wp_parse_args( $assoc_args, array( 'plugin_slug' => $plugin_slug, 'plugin_name' => $plugin_name, 'plugin_package' => $plugin_package, @@ -665,9 +635,8 @@ public function plugin( $args, $assoc_args ) { 'plugin_author' => 'YOUR NAME HERE', 'plugin_author_uri' => 'YOUR SITE HERE', 'plugin_uri' => 'PLUGIN SITE HERE', - 'plugin_tested_up_to' => get_bloginfo( 'version' ), - ]; - $data = wp_parse_args( $assoc_args, $defaults ); + 'plugin_tested_up_to' => get_bloginfo('version'), + ) ); $data['textdomain'] = $plugin_slug; @@ -675,48 +644,45 @@ public function plugin( $args, $assoc_args ) { if ( ! is_dir( $assoc_args['dir'] ) ) { WP_CLI::error( "Cannot create plugin in directory that doesn't exist." ); } - $plugin_dir = "{$assoc_args['dir']}/{$plugin_slug}"; + $plugin_dir = $assoc_args['dir'] . "/$plugin_slug"; } else { - $plugin_dir = WP_PLUGIN_DIR . "/{$plugin_slug}"; + $plugin_dir = WP_PLUGIN_DIR . "/$plugin_slug"; $this->maybe_create_plugins_dir(); - $error_msg = $this->check_target_directory( 'plugin', $plugin_dir ); - if ( ! empty( $error_msg ) ) { + if ( $error_msg = $this->check_target_directory( "plugin", $plugin_dir ) ) { WP_CLI::error( "Invalid plugin slug specified. {$error_msg}" ); } } - $plugin_path = "{$plugin_dir}/{$plugin_slug}.php"; - $plugin_readme_path = "{$plugin_dir}/readme.txt"; - - $files_to_create = [ - $plugin_path => self::mustache_render( 'plugin.mustache', $data ), - $plugin_readme_path => self::mustache_render( 'plugin-readme.mustache', $data ), - "{$plugin_dir}/composer.json" => self::mustache_render( 'plugin-composer.mustache', $data ), - "{$plugin_dir}/.gitignore" => self::mustache_render( 'plugin-gitignore.mustache', $data ), - "{$plugin_dir}/.distignore" => self::mustache_render( 'plugin-distignore.mustache', $data ), - "{$plugin_dir}/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), - ]; - $force = Utils\get_flag_value( $assoc_args, 'force' ); - $files_written = $this->create_files( $files_to_create, $force ); - - $skip_message = 'All plugin files were skipped.'; - $success_message = 'Created plugin files.'; - $this->log_whether_files_written( $files_written, $skip_message, $success_message ); - - if ( ! Utils\get_flag_value( $assoc_args, 'skip-tests' ) ) { - $command_args = [ - 'dir' => $plugin_dir, - 'ci' => empty( $assoc_args['ci'] ) ? '' : $assoc_args['ci'], - 'force' => $force, - ]; - WP_CLI::run_command( [ 'scaffold', 'plugin-tests', $plugin_slug ], $command_args ); + $plugin_path = "$plugin_dir/$plugin_slug.php"; + $plugin_readme_path = "$plugin_dir/readme.txt"; + + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); + $files_written = $this->create_files( array( + $plugin_path => self::mustache_render( 'plugin.mustache', $data ), + $plugin_readme_path => self::mustache_render( 'plugin-readme.mustache', $data ), + "$plugin_dir/package.json" => self::mustache_render( 'plugin-packages.mustache', $data ), + "$plugin_dir/Gruntfile.js" => self::mustache_render( 'plugin-gruntfile.mustache', $data ), + "$plugin_dir/.gitignore" => self::mustache_render( 'plugin-gitignore.mustache', $data ), + "$plugin_dir/.distignore" => self::mustache_render( 'plugin-distignore.mustache', $data ), + "$plugin_dir/.editorconfig" => file_get_contents( self::get_template_path( '.editorconfig' ) ), + ), $force ); + + $this->log_whether_files_written( + $files_written, + $skip_message = 'All plugin files were skipped.', + $success_message = 'Created plugin files.' + ); + + if ( ! \WP_CLI\Utils\get_flag_value( $assoc_args, 'skip-tests' ) ) { + $ci = empty( $assoc_args['ci'] ) ? '' : $assoc_args['ci']; + WP_CLI::run_command( array( 'scaffold', 'plugin-tests', $plugin_slug ), array( 'dir' => $plugin_dir, 'ci' => $ci, 'force' => $force ) ); } - if ( Utils\get_flag_value( $assoc_args, 'activate' ) ) { - WP_CLI::run_command( [ 'plugin', 'activate', $plugin_slug ] ); - } elseif ( Utils\get_flag_value( $assoc_args, 'activate-network' ) ) { - WP_CLI::run_command( [ 'plugin', 'activate', $plugin_slug ], [ 'network' => true ] ); + if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'activate' ) ) { + WP_CLI::run_command( array( 'plugin', 'activate', $plugin_slug ) ); + } else if ( \WP_CLI\Utils\get_flag_value( $assoc_args, 'activate-network' ) ) { + WP_CLI::run_command( array( 'plugin', 'activate', $plugin_slug), array( 'network' => true ) ); } } @@ -726,13 +692,13 @@ public function plugin( $args, $assoc_args ) { * The following files are generated by default: * * * `phpunit.xml.dist` is the configuration file for PHPUnit. - * * `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service. + * * `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * * `tests/bootstrap.php` is the file that makes the current plugin active when running the test suite. * * `tests/test-sample.php` is a sample file containing the actual tests. - * * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. + * * `phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. * - * Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/). + * Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/plugin-unit-tests/). * * ## ENVIRONMENT * @@ -750,12 +716,11 @@ public function plugin( $args, $assoc_args ) { * [--ci=<provider>] * : Choose a configuration file for a continuous integration provider. * --- - * default: circle + * default: travis * options: + * - travis * - circle * - gitlab - * - bitbucket - * - github * --- * * [--force] @@ -779,13 +744,13 @@ public function plugin_tests( $args, $assoc_args ) { * The following files are generated by default: * * * `phpunit.xml.dist` is the configuration file for PHPUnit. - * * `.circleci/config.yml` is the configuration file for CircleCI. Use `--ci=<provider>` to select a different service. + * * `.travis.yml` is the configuration file for Travis CI. Use `--ci=<provider>` to select a different service. * * `bin/install-wp-tests.sh` configures the WordPress test suite and a test database. * * `tests/bootstrap.php` is the file that makes the current theme active when running the test suite. * * `tests/test-sample.php` is a sample file containing the actual tests. - * * `.phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. + * * `phpcs.xml.dist` is a collection of PHP_CodeSniffer rules. * - * Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/misc/plugin-unit-tests/). + * Learn more from the [plugin unit tests documentation](https://make.wordpress.org/cli/handbook/plugin-unit-tests/). * * ## ENVIRONMENT * @@ -803,12 +768,11 @@ public function plugin_tests( $args, $assoc_args ) { * [--ci=<provider>] * : Choose a configuration file for a continuous integration provider. * --- - * default: circle + * default: travis * options: + * - travis * - circle * - gitlab - * - bitbucket - * - github * --- * * [--force] @@ -831,13 +795,9 @@ private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) { if ( ! empty( $args[0] ) ) { $slug = $args[0]; - // Validate slug contains only alphanumeric characters, underscores, and dashes. - if ( in_array( $slug, [ '.', '..' ], true ) ) { + if ( in_array( $slug, array( '.', '..' ) ) ) { WP_CLI::error( "Invalid {$type} slug specified. The slug cannot be '.' or '..'." ); } - if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $slug ) ) { - WP_CLI::error( "Invalid {$type} slug specified. The slug can only contain alphanumeric characters, underscores, and dashes." ); - } if ( 'theme' === $type ) { $theme = wp_get_theme( $slug ); if ( $theme->exists() ) { @@ -846,14 +806,12 @@ private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) { WP_CLI::error( "Invalid {$type} slug specified. The theme '{$slug}' does not exist." ); } } else { - $target_dir = WP_PLUGIN_DIR . "/{$slug}"; + $target_dir = WP_PLUGIN_DIR . "/$slug"; } if ( empty( $assoc_args['dir'] ) && ! is_dir( $target_dir ) ) { WP_CLI::error( "Invalid {$type} slug specified. No such target directory '{$target_dir}'." ); } - - $error_msg = $this->check_target_directory( $type, $target_dir ); - if ( ! empty( $error_msg ) ) { + if ( $error_msg = $this->check_target_directory( $type, $target_dir ) ) { WP_CLI::error( "Invalid {$type} slug specified. {$error_msg}" ); } } @@ -864,14 +822,7 @@ private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) { WP_CLI::error( "Invalid {$type} directory specified. No such directory '{$target_dir}'." ); } if ( empty( $slug ) ) { - $slug = Path::basename( $target_dir ); - // Validate derived slug as well. - if ( in_array( $slug, [ '.', '..' ], true ) ) { - WP_CLI::error( "Invalid {$type} slug specified. The slug cannot be '.' or '..'." ); - } - if ( ! preg_match( '/^[a-zA-Z0-9_-]+$/', $slug ) ) { - WP_CLI::error( "Invalid {$type} slug specified. The slug can only contain alphanumeric characters, underscores, and dashes." ); - } + $slug = Utils\basename( $target_dir ); } } @@ -883,15 +834,15 @@ private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) { $package = str_replace( ' ', '_', $name ); $tests_dir = "{$target_dir}/tests"; - $bin_dir = "{$target_dir}/bin"; + $bin_dir = "{$target_dir}/bin"; $wp_filesystem->mkdir( $tests_dir ); $wp_filesystem->mkdir( $bin_dir ); - $wp_versions_to_test = []; + $wp_versions_to_test = array(); // Parse plugin readme.txt - if ( file_exists( "{$target_dir}/readme.txt" ) ) { - $readme_content = (string) file_get_contents( "{$target_dir}/readme.txt" ); + if ( file_exists( $target_dir . '/readme.txt' ) ) { + $readme_content = file_get_contents( $target_dir . '/readme.txt' ); preg_match( '/Requires at least\:(.*)\n/m', $readme_content, $matches ); if ( isset( $matches[1] ) && $matches[1] ) { @@ -901,61 +852,34 @@ private function scaffold_plugin_theme_tests( $args, $assoc_args, $type ) { $wp_versions_to_test[] = 'latest'; $wp_versions_to_test[] = 'trunk'; - $main_file = "{$slug}.php"; - - if ( 'plugin' === $type ) { - if ( ! function_exists( 'get_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - } - - $all_plugins = get_plugins(); - - if ( ! empty( $all_plugins ) ) { - $filtered = array_filter( - array_keys( $all_plugins ), - static function ( $item ) use ( $slug ) { - return ( false !== strpos( $item, "{$slug}/" ) ); - } - ); - - if ( ! empty( $filtered ) ) { - $main_file = basename( reset( $filtered ) ); - } - } - } - - $template_data = [ - "{$type}_slug" => $slug, - "{$type}_package" => $package, - "{$type}_main_file" => $main_file, - ]; - - $force = Utils\get_flag_value( $assoc_args, 'force' ); - $files_to_create = [ - "{$tests_dir}/bootstrap.php" => self::mustache_render( "{$type}-bootstrap.mustache", $template_data ), - "{$tests_dir}/test-sample.php" => self::mustache_render( "{$type}-test-sample.mustache", $template_data ), - ]; - if ( 'circle' === $assoc_args['ci'] ) { - $files_to_create[ "{$target_dir}/.circleci/config.yml" ] = self::mustache_render( 'plugin-circle.mustache', compact( 'wp_versions_to_test' ) ); - } elseif ( 'gitlab' === $assoc_args['ci'] ) { - $files_to_create[ "{$target_dir}/.gitlab-ci.yml" ] = self::mustache_render( 'plugin-gitlab.mustache' ); - } elseif ( 'bitbucket' === $assoc_args['ci'] ) { - $files_to_create[ "{$target_dir}/bitbucket-pipelines.yml" ] = self::mustache_render( 'plugin-bitbucket.mustache' ); - } elseif ( 'github' === $assoc_args['ci'] ) { - $files_to_create[ "{$target_dir}/.github/workflows/testing.yml" ] = self::mustache_render( 'plugin-github.mustache' ); + $template_data = array( + "{$type}_slug" => $slug, + "{$type}_package" => $package, + ); + + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); + $files_to_create = array( + "$tests_dir/bootstrap.php" => self::mustache_render( "{$type}-bootstrap.mustache", $template_data ), + "$tests_dir/test-sample.php" => self::mustache_render( "{$type}-test-sample.mustache", $template_data ), + ); + if ( 'travis' === $assoc_args['ci'] ) { + $files_to_create["{$target_dir}/.travis.yml"] = self::mustache_render( 'plugin-travis.mustache', compact( 'wp_versions_to_test' ) ); + } else if ( 'circle' === $assoc_args['ci'] ) { + $files_to_create["{$target_dir}/.circleci/config.yml"] = self::mustache_render( 'plugin-circle.mustache', compact( 'wp_versions_to_test' ) ); + } else if ( 'gitlab' === $assoc_args['ci'] ) { + $files_to_create["{$target_dir}/.gitlab-ci.yml"] = self::mustache_render( 'plugin-gitlab.mustache' ); } - $files_written = $this->create_files( $files_to_create, $force ); - $to_copy = [ + $to_copy = array( 'install-wp-tests.sh' => $bin_dir, 'phpunit.xml.dist' => $target_dir, - '.phpcs.xml.dist' => $target_dir, - ]; + 'phpcs.xml.dist' => $target_dir, + ); foreach ( $to_copy as $file => $dir ) { - $file_name = "{$dir}/{$file}"; - $force = Utils\get_flag_value( $assoc_args, 'force' ); + $file_name = "$dir/$file"; + $force = \WP_CLI\Utils\get_flag_value( $assoc_args, 'force' ); $should_write_file = $this->prompt_if_files_will_be_overwritten( $file_name, $force ); if ( ! $should_write_file ) { continue; @@ -964,15 +888,16 @@ static function ( $item ) use ( $slug ) { $wp_filesystem->copy( self::get_template_path( $file ), $file_name, true ); if ( 'install-wp-tests.sh' === $file ) { - if ( ! $wp_filesystem->chmod( "{$dir}/{$file}", 0755 ) ) { + if ( ! $wp_filesystem->chmod( "$dir/$file", 0755 ) ) { WP_CLI::warning( "Couldn't mark 'install-wp-tests.sh' as executable." ); } } } - - $skip_message = 'All test files were skipped.'; - $success_message = 'Created test files.'; - $this->log_whether_files_written( $files_written, $skip_message, $success_message ); + $this->log_whether_files_written( + $files_written, + $skip_message = 'All test files were skipped.', + $success_message = 'Created test files.' + ); } /** @@ -986,14 +911,12 @@ static function ( $item ) use ( $slug ) { private function check_target_directory( $type, $target_dir ) { $parent_dir = dirname( self::canonicalize_path( str_replace( '\\', '/', $target_dir ) ) ); - $themes_dir = self::canonicalize_path( str_replace( '\\', '/', WP_CONTENT_DIR . '/themes' ) ); - if ( 'theme' === $type && $themes_dir !== $parent_dir ) { - return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, $themes_dir ); + if ( 'theme' === $type && str_replace( '\\', '/', WP_CONTENT_DIR . '/themes' ) !== $parent_dir ) { + return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, WP_CONTENT_DIR . '/themes' ); } - $plugins_dir = self::canonicalize_path( str_replace( '\\', '/', WP_PLUGIN_DIR ) ); - if ( 'plugin' === $type && $plugins_dir !== $parent_dir ) { - return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, $plugins_dir ); + if ( 'plugin' === $type && str_replace( '\\', '/', WP_PLUGIN_DIR ) !== $parent_dir ) { + return sprintf( 'The target directory \'%1$s\' is not in \'%2$s\'.', $target_dir, WP_PLUGIN_DIR ); } // Success. @@ -1002,7 +925,7 @@ private function check_target_directory( $type, $target_dir ) { protected function create_files( $files_and_contents, $force ) { $wp_filesystem = $this->init_wp_filesystem(); - $wrote_files = []; + $wrote_files = array(); foreach ( $files_and_contents as $filename => $contents ) { $should_write_file = $this->prompt_if_files_will_be_overwritten( $filename, $force ); @@ -1010,18 +933,10 @@ protected function create_files( $files_and_contents, $force ) { continue; } - $contents = str_replace( "\r\n", "\n", $contents ); - $wp_filesystem->mkdir( dirname( $filename ) ); - // Create multi-level folders. - if ( false === $wp_filesystem->exists( dirname( $filename ) ) ) { - $wp_filesystem->mkdir( dirname( dirname( $filename ) ) ); - $wp_filesystem->mkdir( dirname( $filename ) ); - } - if ( ! $wp_filesystem->put_contents( $filename, $contents ) ) { - WP_CLI::error( "Error creating file: {$filename}" ); + WP_CLI::error( "Error creating file: $filename" ); } elseif ( $should_write_file ) { $wrote_files[] = $filename; } @@ -1039,12 +954,12 @@ protected function prompt_if_files_will_be_overwritten( $filename, $force ) { WP_CLI::log( $filename ); if ( ! $force ) { do { - $answer = cli\prompt( + $answer = cli\prompt( 'Skip this file, or replace it with scaffolding?', $default = false, - $marker = '[s/r]: ' + $marker = '[s/r]: ' ); - } while ( ! in_array( $answer, [ 's', 'r' ], true ) ); + } while ( ! in_array( $answer, array( 's', 'r' ) ) ); $should_write_file = 'r' === $answer; } @@ -1065,11 +980,11 @@ protected function log_whether_files_written( $files_written, $skip_message, $su /** * Extracts dashicon name when provided or return null otherwise. * - * @param array{dashicon?: string} $assoc_args + * @param array $assoc_args * @return string|null */ private function extract_dashicon( $assoc_args ) { - $dashicon = Utils\get_flag_value( $assoc_args, 'dashicon' ); + $dashicon = \WP_CLI\Utils\get_flag_value( $assoc_args, 'dashicon' ); if ( ! $dashicon ) { return null; } @@ -1118,10 +1033,10 @@ private function pluralize( $word ) { } protected function extract_args( $assoc_args, $defaults ) { - $out = []; + $out = array(); foreach ( $defaults as $key => $value ) { - $out[ $key ] = Utils\get_flag_value( $assoc_args, $key, $value ); + $out[ $key ] = \WP_CLI\Utils\get_flag_value( $assoc_args, $key, $value ); } return $out; @@ -1140,6 +1055,7 @@ protected function maybe_create_themes_dir() { if ( ! is_dir( $themes_dir ) ) { wp_mkdir_p( $themes_dir ); } + } /** @@ -1150,6 +1066,7 @@ protected function maybe_create_plugins_dir() { if ( ! is_dir( WP_PLUGIN_DIR ) ) { wp_mkdir_p( WP_PLUGIN_DIR ); } + } /** @@ -1165,15 +1082,15 @@ protected function init_wp_filesystem() { /** * Localizes the template path. */ - private static function mustache_render( $template, $data = [] ) { - return Utils\mustache_render( dirname( __DIR__ ) . "/templates/{$template}", $data ); + private static function mustache_render( $template, $data = array() ) { + return Utils\mustache_render( dirname( dirname( __FILE__ ) ) . '/templates/' . $template, $data ); } /** * Gets the template path based on installation type. */ private static function get_template_path( $template ) { - $command_root = Path::phar_safe( dirname( __DIR__ ) ); + $command_root = Utils\phar_safe_path( dirname( __DIR__ ) ); $template_path = "{$command_root}/templates/{$template}"; if ( ! file_exists( $template_path ) ) { @@ -1202,7 +1119,7 @@ private static function canonicalize_path( $path ) { $path .= '/'; } - $output = []; + $output = array(); foreach ( explode( '/', $path ) as $segment ) { if ( '..' === $segment ) { @@ -1218,7 +1135,7 @@ private static function canonicalize_path( $path ) { /** * Gets an active theme's name when true provided or the same name otherwise. * - * @param string|true $theme Theme name or true. + * @param string|bool $theme Theme name or true. * @return string */ private function get_theme_name( $theme ) { diff --git a/src/WP_CLI/Inflector.php b/src/WP_CLI/Inflector.php new file mode 100644 index 000000000..664fbb913 --- /dev/null +++ b/src/WP_CLI/Inflector.php @@ -0,0 +1,483 @@ +<?php +/* + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * This software consists of voluntary contributions made by many individuals + * and is licensed under the MIT license. For more information, see + * <http://www.doctrine-project.org>. + */ + +namespace WP_CLI; +use WP_CLI; + +/** + * Doctrine inflector has static methods for inflecting text. + * + * The methods in these classes are from several different sources collected + * across several different php projects and several different authors. The + * original author names and emails are not known. + * + * Pluralize & Singularize implementation are borrowed from CakePHP with some modifications. + * + * @link www.doctrine-project.org + * @since 1.0 + * @author Konsta Vesterinen <kvesteri@cc.hut.fi> + * @author Jonathan H. Wage <jonwage@gmail.com> + */ +class Inflector +{ + /** + * Plural inflector rules. + * + * @var array + */ + private static $plural = array( + 'rules' => array( + '/(s)tatus$/i' => '\1\2tatuses', + '/(quiz)$/i' => '\1zes', + '/^(ox)$/i' => '\1\2en', + '/([m|l])ouse$/i' => '\1ice', + '/(matr|vert|ind)(ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])um$/i' => '\1a', + '/(p)erson$/i' => '\1eople', + '/(m)an$/i' => '\1en', + '/(c)hild$/i' => '\1hildren', + '/(f)oot$/i' => '\1eet', + '/(buffal|her|potat|tomat|volcan)o$/i' => '\1\2oes', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i', + '/us$/i' => 'uses', + '/(alias)$/i' => '\1es', + '/(analys|ax|cris|test|thes)is$/i' => '\1es', + '/s$/' => 's', + '/^$/' => '', + '/$/' => 's', + ), + 'uninflected' => array( + '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep', 'people', 'cookie' + ), + 'irregular' => array( + 'atlas' => 'atlases', + 'axe' => 'axes', + 'beef' => 'beefs', + 'brother' => 'brothers', + 'cafe' => 'cafes', + 'chateau' => 'chateaux', + 'child' => 'children', + 'cookie' => 'cookies', + 'corpus' => 'corpuses', + 'cow' => 'cows', + 'criterion' => 'criteria', + 'curriculum' => 'curricula', + 'demo' => 'demos', + 'domino' => 'dominoes', + 'echo' => 'echoes', + 'foot' => 'feet', + 'fungus' => 'fungi', + 'ganglion' => 'ganglions', + 'genie' => 'genies', + 'genus' => 'genera', + 'graffito' => 'graffiti', + 'hippopotamus' => 'hippopotami', + 'hoof' => 'hoofs', + 'human' => 'humans', + 'iris' => 'irises', + 'leaf' => 'leaves', + 'loaf' => 'loaves', + 'man' => 'men', + 'medium' => 'media', + 'memorandum' => 'memoranda', + 'money' => 'monies', + 'mongoose' => 'mongooses', + 'motto' => 'mottoes', + 'move' => 'moves', + 'mythos' => 'mythoi', + 'niche' => 'niches', + 'nucleus' => 'nuclei', + 'numen' => 'numina', + 'occiput' => 'occiputs', + 'octopus' => 'octopuses', + 'opus' => 'opuses', + 'ox' => 'oxen', + 'penis' => 'penises', + 'person' => 'people', + 'plateau' => 'plateaux', + 'runner-up' => 'runners-up', + 'sex' => 'sexes', + 'soliloquy' => 'soliloquies', + 'son-in-law' => 'sons-in-law', + 'syllabus' => 'syllabi', + 'testis' => 'testes', + 'thief' => 'thieves', + 'tooth' => 'teeth', + 'tornado' => 'tornadoes', + 'trilby' => 'trilbys', + 'turf' => 'turfs', + 'volcano' => 'volcanoes', + ) + ); + + /** + * Singular inflector rules. + * + * @var array + */ + private static $singular = array( + 'rules' => array( + '/(s)tatuses$/i' => '\1\2tatus', + '/^(.*)(menu)s$/i' => '\1\2', + '/(quiz)zes$/i' => '\\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias)(es)*$/i' => '\1', + '/(buffal|her|potat|tomat|volcan)oes$/i' => '\1o', + '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us', + '/([ftw]ax)es/i' => '\1', + '/(analys|ax|cris|test|thes)es$/i' => '\1is', + '/(shoe|slave)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/ouses$/' => 'ouse', + '/([^a])uses$/' => '\1us', + '/([m|l])ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1\2ovie', + '/(s)eries$/i' => '\1\2eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([lr])ves$/i' => '\1f', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/(drive)s$/i' => '\1', + '/([^fo])ves$/i' => '\1fe', + '/(^analy)ses$/i' => '\1sis', + '/(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis', + '/([ti])a$/i' => '\1um', + '/(p)eople$/i' => '\1\2erson', + '/(m)en$/i' => '\1an', + '/(c)hildren$/i' => '\1\2hild', + '/(f)eet$/i' => '\1oot', + '/(n)ews$/i' => '\1\2ews', + '/eaus$/' => 'eau', + '/^(.*us)$/' => '\\1', + '/s$/i' => '', + ), + 'uninflected' => array( + '.*[nrlm]ese', + '.*deer', + '.*fish', + '.*measles', + '.*ois', + '.*pox', + '.*sheep', + '.*ss', + ), + 'irregular' => array( + 'criteria' => 'criterion', + 'curves' => 'curve', + 'emphases' => 'emphasis', + 'foes' => 'foe', + 'hoaxes' => 'hoax', + 'media' => 'medium', + 'neuroses' => 'neurosis', + 'waves' => 'wave', + 'oases' => 'oasis', + ) + ); + + /** + * Words that should not be inflected. + * + * @var array + */ + private static $uninflected = array( + 'Amoyese', 'bison', 'Borghese', 'bream', 'breeches', 'britches', 'buffalo', 'cantus', + 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'Congoese', 'contretemps', 'corps', + 'debris', 'diabetes', 'djinn', 'eland', 'elk', 'equipment', 'Faroese', 'flounder', + 'Foochowese', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'graffiti', + 'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings', + 'jackanapes', 'Kiplingese', 'Kongoese', 'Lucchese', 'mackerel', 'Maltese', '.*?media', + 'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', + 'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'pliers', 'Portuguese', + 'proceedings', 'rabies', 'rice', 'rhinoceros', 'salmon', 'Sarawakese', 'scissors', + 'sea[- ]bass', 'series', 'Shavese', 'shears', 'siemens', 'species', 'staff', 'swine', + 'testes', 'trousers', 'trout', 'tuna', 'Vermontese', 'Wenchowese', 'whiting', + 'wildebeest', 'Yengeese' + ); + + /** + * Method cache array. + * + * @var array + */ + private static $cache = array(); + + /** + * The initial state of Inflector so reset() works. + * + * @var array + */ + private static $initialState = array(); + + /** + * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'. + * + * @param string $word The word to tableize. + * + * @return string The tableized word. + */ + public static function tableize($word) + { + return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '_$1', $word)); + } + + /** + * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'. + * + * @param string $word The word to classify. + * + * @return string The classified word. + */ + public static function classify($word) + { + return str_replace(" ", "", ucwords(strtr($word, "_-", " "))); + } + + /** + * Camelizes a word. This uses the classify() method and turns the first character to lowercase. + * + * @param string $word The word to camelize. + * + * @return string The camelized word. + */ + public static function camelize($word) + { + return lcfirst(self::classify($word)); + } + + /** + * Uppercases words with configurable delimeters between words. + * + * Takes a string and capitalizes all of the words, like PHP's built-in + * ucwords function. This extends that behavior, however, by allowing the + * word delimeters to be configured, rather than only separating on + * whitespace. + * + * Here is an example: + * <code> + * <?php + * $string = 'top-o-the-morning to all_of_you!'; + * echo \Doctrine\Common\Inflector\Inflector::ucwords($string); + * // Top-O-The-Morning To All_of_you! + * + * echo \Doctrine\Common\Inflector\Inflector::ucwords($string, '-_ '); + * // Top-O-The-Morning To All_Of_You! + * ?> + * </code> + * + * @param string $string The string to operate on. + * @param string $delimiters A list of word separators. + * + * @return string The string with all delimeter-separated words capitalized. + */ + public static function ucwords($string, $delimiters = " \n\t\r\0\x0B-") + { + return preg_replace_callback( + '/[^' . preg_quote($delimiters, '/') . ']+/', + function($matches) { + return ucfirst($matches[0]); + }, + $string + ); + } + + /** + * Clears Inflectors inflected value caches, and resets the inflection + * rules to the initial values. + * + * @return void + */ + public static function reset() + { + if (empty(self::$initialState)) { + self::$initialState = get_class_vars('Inflector'); + + return; + } + + foreach (self::$initialState as $key => $val) { + if ($key != 'initialState') { + self::${$key} = $val; + } + } + } + + /** + * Adds custom inflection $rules, of either 'plural' or 'singular' $type. + * + * ### Usage: + * + * {{{ + * Inflector::rules('plural', array('/^(inflect)or$/i' => '\1ables')); + * Inflector::rules('plural', array( + * 'rules' => array('/^(inflect)ors$/i' => '\1ables'), + * 'uninflected' => array('dontinflectme'), + * 'irregular' => array('red' => 'redlings') + * )); + * }}} + * + * @param string $type The type of inflection, either 'plural' or 'singular' + * @param array $rules An array of rules to be added. + * @param boolean $reset If true, will unset default inflections for all + * new rules that are being defined in $rules. + * + * @return void + */ + public static function rules($type, $rules, $reset = false) + { + foreach ($rules as $rule => $pattern) { + if ( ! is_array($pattern)) { + continue; + } + + if ($reset) { + self::${$type}[$rule] = $pattern; + } else { + self::${$type}[$rule] = ($rule === 'uninflected') + ? array_merge($pattern, self::${$type}[$rule]) + : $pattern + self::${$type}[$rule]; + } + + unset($rules[$rule], self::${$type}['cache' . ucfirst($rule)]); + + if (isset(self::${$type}['merged'][$rule])) { + unset(self::${$type}['merged'][$rule]); + } + + if ($type === 'plural') { + self::$cache['pluralize'] = self::$cache['tableize'] = array(); + } elseif ($type === 'singular') { + self::$cache['singularize'] = array(); + } + } + + self::${$type}['rules'] = $rules + self::${$type}['rules']; + } + + /** + * Returns a word in plural form. + * + * @param string $word The word in singular form. + * + * @return string The word in plural form. + */ + public static function pluralize($word) + { + if (isset(self::$cache['pluralize'][$word])) { + return self::$cache['pluralize'][$word]; + } + + if (!isset(self::$plural['merged']['irregular'])) { + self::$plural['merged']['irregular'] = self::$plural['irregular']; + } + + if (!isset(self::$plural['merged']['uninflected'])) { + self::$plural['merged']['uninflected'] = array_merge(self::$plural['uninflected'], self::$uninflected); + } + + if (!isset(self::$plural['cacheUninflected']) || !isset(self::$plural['cacheIrregular'])) { + self::$plural['cacheUninflected'] = '(?:' . implode('|', self::$plural['merged']['uninflected']) . ')'; + self::$plural['cacheIrregular'] = '(?:' . implode('|', array_keys(self::$plural['merged']['irregular'])) . ')'; + } + + if (preg_match('/(.*)\\b(' . self::$plural['cacheIrregular'] . ')$/i', $word, $regs)) { + self::$cache['pluralize'][$word] = $regs[1] . substr($word, 0, 1) . substr(self::$plural['merged']['irregular'][strtolower($regs[2])], 1); + + return self::$cache['pluralize'][$word]; + } + + if (preg_match('/^(' . self::$plural['cacheUninflected'] . ')$/i', $word, $regs)) { + self::$cache['pluralize'][$word] = $word; + + return $word; + } + + foreach (self::$plural['rules'] as $rule => $replacement) { + if (preg_match($rule, $word)) { + self::$cache['pluralize'][$word] = preg_replace($rule, $replacement, $word); + + return self::$cache['pluralize'][$word]; + } + } + } + + /** + * Returns a word in singular form. + * + * @param string $word The word in plural form. + * + * @return string The word in singular form. + */ + public static function singularize($word) + { + if (isset(self::$cache['singularize'][$word])) { + return self::$cache['singularize'][$word]; + } + + if (!isset(self::$singular['merged']['uninflected'])) { + self::$singular['merged']['uninflected'] = array_merge( + self::$singular['uninflected'], + self::$uninflected + ); + } + + if (!isset(self::$singular['merged']['irregular'])) { + self::$singular['merged']['irregular'] = array_merge( + self::$singular['irregular'], + array_flip(self::$plural['irregular']) + ); + } + + if (!isset(self::$singular['cacheUninflected']) || !isset(self::$singular['cacheIrregular'])) { + self::$singular['cacheUninflected'] = '(?:' . join('|', self::$singular['merged']['uninflected']) . ')'; + self::$singular['cacheIrregular'] = '(?:' . join('|', array_keys(self::$singular['merged']['irregular'])) . ')'; + } + + if (preg_match('/(.*)\\b(' . self::$singular['cacheIrregular'] . ')$/i', $word, $regs)) { + self::$cache['singularize'][$word] = $regs[1] . substr($word, 0, 1) . substr(self::$singular['merged']['irregular'][strtolower($regs[2])], 1); + + return self::$cache['singularize'][$word]; + } + + if (preg_match('/^(' . self::$singular['cacheUninflected'] . ')$/i', $word, $regs)) { + self::$cache['singularize'][$word] = $word; + + return $word; + } + + foreach (self::$singular['rules'] as $rule => $replacement) { + if (preg_match($rule, $word)) { + self::$cache['singularize'][$word] = preg_replace($rule, $replacement, $word); + + return self::$cache['singularize'][$word]; + } + } + + self::$cache['singularize'][$word] = $word; + + return $word; + } +} diff --git a/templates/.phpcs.xml.dist b/templates/.phpcs.xml.dist deleted file mode 100644 index 1c6774448..000000000 --- a/templates/.phpcs.xml.dist +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0"?> -<ruleset name="WordPress Coding Standards based custom ruleset for your plugin"> - <description>Generally-applicable sniffs for WordPress plugins.</description> - - <!-- What to scan --> - <file>.</file> - <exclude-pattern>/vendor/</exclude-pattern> - <exclude-pattern>/node_modules/</exclude-pattern> - - <!-- How to scan --> - <!-- Usage instructions: https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Usage --> - <!-- Annotated ruleset: https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki/Annotated-ruleset.xml --> - <arg value="sp"/> <!-- Show sniff and progress --> - <arg name="basepath" value="./"/><!-- Strip the file paths down to the relevant bit --> - <arg name="colors"/> - <arg name="extensions" value="php"/> - <arg name="parallel" value="8"/><!-- Enables parallel processing when available for faster results. --> - - <!-- Rules: Check PHP version compatibility --> - <!-- https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions --> - <config name="testVersion" value="7.2-"/> - <!-- https://github.com/PHPCompatibility/PHPCompatibilityWP --> - <rule ref="PHPCompatibilityWP"/> - - <!-- Rules: WordPress Coding Standards --> - <!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards --> - <!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties --> - <config name="minimum_supported_wp_version" value="4.6"/> - <rule ref="WordPress"/> - <rule ref="WordPress.NamingConventions.PrefixAllGlobals"> - <properties> - <!-- Value: replace the function, class, and variable prefixes used. Separate multiple prefixes with a comma. --> - <property name="prefixes" type="array" value="my-plugin"/> - </properties> - </rule> - <rule ref="WordPress.WP.I18n"> - <properties> - <!-- Value: replace the text domain used. --> - <property name="text_domain" type="array" value="my-plugin"/> - </properties> - </rule> - <rule ref="WordPress.WhiteSpace.ControlStructureSpacing"> - <properties> - <property name="blank_line_check" value="true"/> - </properties> - </rule> -</ruleset> diff --git a/templates/block-index-js.mustache b/templates/block-block-js.mustache similarity index 69% rename from templates/block-index-js.mustache rename to templates/block-block-js.mustache index 60c4f5efb..722763187 100644 --- a/templates/block-index-js.mustache +++ b/templates/block-block-js.mustache @@ -1,30 +1,30 @@ ( function( wp ) { /** * Registers a new block provided a unique name and an object defining its behavior. - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/#registering-a-block + * @see https://github.com/WordPress/gutenberg/tree/master/blocks#api */ var registerBlockType = wp.blocks.registerBlockType; /** * Returns a new element of given type. Element is an abstraction layer atop React. - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/packages/packages-element/ + * @see https://github.com/WordPress/gutenberg/tree/master/element#element */ var el = wp.element.createElement; /** * Retrieves the translation of text. - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/packages/packages-i18n/ + * @see https://github.com/WordPress/gutenberg/tree/master/i18n#api */ var __ = wp.i18n.__; /** * Every block starts by registering a new block type definition. - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/#registering-a-block + * @see https://wordpress.org/gutenberg/handbook/block-api/ */ registerBlockType( '{{namespace}}/{{slug}}', { /** * This is the display title for your block, which can be translated with `i18n` functions. * The block inserter will show this name. */ - title: __( '{{title_ucfirst_js}}', '{{namespace}}' ), + title: __( '{{title_ucfirst}}' ), {{#dashicon}} /** @@ -51,7 +51,7 @@ /** * The edit function describes the structure of your block in the context of the editor. * This represents what the editor will render when the block is used. - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#edit + * @see https://wordpress.org/gutenberg/handbook/block-edit-save/#edit * * @param {Object} [props] Properties passed from the editor. * @return {Element} Element to render. @@ -60,14 +60,14 @@ return el( 'p', { className: props.className }, - __( 'Hello from the editor!', '{{namespace}}' ) + __( 'Hello from the editor!' ) ); }, /** * The save function defines the way in which the different attributes should be combined * into the final markup, which is then serialized by Gutenberg into `post_content`. - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/block-api/block-edit-save/#save + * @see https://wordpress.org/gutenberg/handbook/block-edit-save/#save * * @return {Element} Element to render. */ @@ -75,7 +75,7 @@ return el( 'p', {}, - __( 'Hello from the saved content!', '{{namespace}}' ) + __( 'Hello from the saved content!' ) ); } } ); diff --git a/templates/block-php.mustache b/templates/block-php.mustache index f85813807..d09fc4668 100644 --- a/templates/block-php.mustache +++ b/templates/block-php.mustache @@ -10,72 +10,47 @@ * Registers all block assets so that they can be enqueued through Gutenberg in * the corresponding context. * - * @see https://wordpress.org/gutenberg/handbook/designers-developers/developers/tutorials/block-tutorial/applying-styles-with-stylesheets/ + * @see https://wordpress.org/gutenberg/handbook/blocks/writing-your-first-block-type/#enqueuing-block-scripts */ function {{machine_name}}_block_init() { - // Skip block registration if Gutenberg is not enabled/merged. - if ( ! function_exists( 'register_block_type' ) ) { - return; - } - {{#plugin}} - $dir = __DIR__; - {{/plugin}} - {{#theme}} - $dir = get_stylesheet_directory() . '/blocks'; - {{/theme}} + $dir = dirname( __FILE__ ); - $index_js = '{{slug}}/index.js'; + $block_js = '{{slug}}/block.js'; wp_register_script( '{{slug}}-block-editor', - {{#plugin}} - plugins_url( $index_js, __FILE__ ), - {{/plugin}} - {{#theme}} - get_stylesheet_directory_uri() . "/blocks/{$index_js}", - {{/theme}} + plugins_url( $block_js, __FILE__ ), array( 'wp-blocks', 'wp-i18n', 'wp-element', ), - filemtime( "{$dir}/{$index_js}" ), - false + filemtime( "$dir/$block_js" ) ); $editor_css = '{{slug}}/editor.css'; wp_register_style( '{{slug}}-block-editor', - {{#plugin}} plugins_url( $editor_css, __FILE__ ), - {{/plugin}} - {{#theme}} - get_stylesheet_directory_uri() . "/blocks/{$editor_css}", - {{/theme}} - array(), - filemtime( "{$dir}/{$editor_css}" ) + array( + 'wp-blocks', + ), + filemtime( "$dir/$editor_css" ) ); $style_css = '{{slug}}/style.css'; wp_register_style( '{{slug}}-block', - {{#plugin}} plugins_url( $style_css, __FILE__ ), - {{/plugin}} - {{#theme}} - get_stylesheet_directory_uri() . "/blocks/{$style_css}", - {{/theme}} - array(), - filemtime( "{$dir}/{$style_css}" ) - ); - - register_block_type( - '{{namespace}}/{{slug}}', array( - 'editor_script' => '{{slug}}-block-editor', - 'editor_style' => '{{slug}}-block-editor', - 'style' => '{{slug}}-block', - ) + 'wp-blocks', + ), + filemtime( "$dir/$style_css" ) ); -} + register_block_type( '{{namespace}}/{{slug}}', array( + 'editor_script' => '{{slug}}-block-editor', + 'editor_style' => '{{slug}}-block-editor', + 'style' => '{{slug}}-block', + ) ); +} add_action( 'init', '{{machine_name}}_block_init' ); diff --git a/templates/child_theme_functions.mustache b/templates/child_theme_functions.mustache index 34ba77a67..b3486ce6c 100644 --- a/templates/child_theme_functions.mustache +++ b/templates/child_theme_functions.mustache @@ -1,24 +1,22 @@ <?php /** - * {{theme_name}} Theme functions and definitions. + * {{theme_name}} Theme functions and definitions * * @link https://developer.wordpress.org/themes/basics/theme-functions/ * * @package {{slug}} */ -add_action( 'wp_enqueue_scripts', '{{prefix_safe}}_parent_theme_enqueue_styles' ); +add_action( 'wp_enqueue_scripts', '{{parent_theme_function_safe}}_parent_theme_enqueue_styles' ); /** * Enqueue scripts and styles. */ -function {{prefix_safe}}_parent_theme_enqueue_styles() { - wp_enqueue_style( '{{parent_theme}}-style', get_template_directory_uri() . '/style.css', array(), '0.1.0' ); - wp_enqueue_style( - '{{slug}}-style', +function {{parent_theme_function_safe}}_parent_theme_enqueue_styles() { + wp_enqueue_style( '{{parent_theme}}-style', get_template_directory_uri() . '/style.css' ); + wp_enqueue_style( '{{slug}}-style', get_stylesheet_directory_uri() . '/style.css', - array( '{{parent_theme}}-style' ), - '0.1.0' + array( '{{parent_theme}}-style' ) ); - wp_style_add_data( '{{slug}}-style', 'rtl', 'add' ); + } diff --git a/templates/install-wp-tests.sh b/templates/install-wp-tests.sh index 509919caa..878881f03 100644 --- a/templates/install-wp-tests.sh +++ b/templates/install-wp-tests.sh @@ -1,16 +1,7 @@ #!/usr/bin/env bash -# See https://raw.githubusercontent.com/wp-cli/scaffold-command/master/templates/install-wp-tests.sh - -# Set up colors for output -RED="\033[0;31m" -GREEN="\033[0;32m" -YELLOW="\033[0;33m" -CYAN="\033[0;36m" -RESET="\033[0m" - if [ $# -lt 3 ]; then - echo -e "${YELLOW}Usage:${RESET} $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]" + echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]" exit 1 fi @@ -24,68 +15,17 @@ SKIP_DB_CREATE=${6-false} TMPDIR=${TMPDIR-/tmp} TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} -WP_TESTS_FILE="$WP_TESTS_DIR"/includes/functions.php -WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress} -WP_CORE_FILE="$WP_CORE_DIR"/wp-settings.php +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} download() { - if command -v curl > /dev/null 2>&1; then - curl -L -s "$1" > "$2"; - return $? - elif command -v wget > /dev/null 2>&1; then + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then wget -nv -O "$2" "$1" - return $? - else - echo -e "${RED}Error: Neither curl nor wget is installed.${RESET}" - exit 1 fi } -check_for_updates() { - local remote_url="https://raw.githubusercontent.com/wp-cli/scaffold-command/main/templates/install-wp-tests.sh" - local tmp_script="$TMPDIR/install-wp-tests.sh.latest" - - if ! download "$remote_url" "$tmp_script"; then - echo -e "${YELLOW}Warning: Failed to download the latest version of the script for update check.${RESET}" - return - fi - - if [ ! -f "$tmp_script" ] || [ ! -s "$tmp_script" ]; then - echo -e "${YELLOW}Warning: Downloaded script is missing or empty, cannot check for updates.${RESET}" - rm -f "$tmp_script" - return - fi - - local local_hash="" - local remote_hash="" - - if command -v shasum > /dev/null; then - local_hash=$(shasum -a 256 "$0" | awk '{print $1}') - remote_hash=$(shasum -a 256 "$tmp_script" | awk '{print $1}') - elif command -v sha256sum > /dev/null; then - local_hash=$(sha256sum "$0" | awk '{print $1}') - remote_hash=$(sha256sum "$tmp_script" | awk '{print $1}') - else - echo -e "${YELLOW}Warning: Could not find shasum or sha256sum to check for script updates.${RESET}" - rm "$tmp_script" - return - fi - - rm "$tmp_script" - - if [ "$local_hash" != "$remote_hash" ]; then - echo -e "${YELLOW}Warning: A newer version of this script is available at $remote_url${RESET}" - fi -} -# Allow disabling the update check by setting WP_INSTALL_TESTS_SKIP_UPDATE_CHECK=true in the environment. -if [ "${WP_INSTALL_TESTS_SKIP_UPDATE_CHECK:-false}" != "true" ]; then - check_for_updates -fi - -if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then - WP_BRANCH=${WP_VERSION%\-*} - WP_TESTS_TAG="branches/$WP_BRANCH" -elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then WP_TESTS_TAG="branches/$WP_VERSION" elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then @@ -99,15 +39,12 @@ elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then else # http serves a single offer, whereas https serves multiple. we only want one download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json - LATEST_VERSION=$(grep -oE '"version":"[^"]*' /tmp/wp-latest.json | head -n 1 | sed 's/"version":"//') + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') if [[ -z "$LATEST_VERSION" ]]; then - echo -e "${RED}Error: Latest WordPress version could not be found.${RESET}" + echo "Latest WordPress version could not be found" exit 1 fi - # The version-check endpoint returns major.minor (e.g., 6.9), but GitHub tags include the patch version (e.g., 6.9.0) - if [[ $LATEST_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then - LATEST_VERSION="${LATEST_VERSION}.0" - fi WP_TESTS_TAG="tags/$LATEST_VERSION" fi @@ -115,19 +52,17 @@ set -ex install_wp() { - if [ -f $WP_CORE_FILE ]; then - echo -e "${CYAN}WordPress is already installed.${RESET}" + if [ -d $WP_CORE_DIR ]; then return; fi - echo -e "${CYAN}Installing WordPress...${RESET}" - - rm -rf $WP_CORE_DIR mkdir -p $WP_CORE_DIR if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then - download https://github.com/WordPress/wordpress/archive/refs/heads/master.tar.gz $TMPDIR/wordpress.tar.gz - tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR else if [ $WP_VERSION == 'latest' ]; then local ARCHIVE_NAME='latest' @@ -153,126 +88,42 @@ install_wp() { download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR fi - echo -e "${GREEN}WordPress installed successfully.${RESET}" + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php } install_test_suite() { # portable in-place argument for both GNU sed and Mac OSX sed if [[ $(uname -s) == 'Darwin' ]]; then - local ioption='-i.bak' + local ioption='-i .bak' else local ioption='-i' fi - # set up testing suite if it doesn't yet exist or only partially exists - if [ ! -f $WP_TESTS_FILE ]; then - echo -e "${CYAN}Installing test suite...${RESET}" + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then # set up testing suite - rm -rf $WP_TESTS_DIR mkdir -p $WP_TESTS_DIR - - if [[ $WP_TESTS_TAG == 'trunk' ]]; then - ref=trunk - archive_url="https://github.com/WordPress/wordpress-develop/archive/refs/heads/${ref}.tar.gz" - elif [[ $WP_TESTS_TAG == branches/* ]]; then - ref=${WP_TESTS_TAG#branches/} - archive_url="https://github.com/WordPress/wordpress-develop/archive/refs/heads/${ref}.tar.gz" - else - ref=${WP_TESTS_TAG#tags/} - archive_url="https://github.com/WordPress/wordpress-develop/archive/refs/tags/${ref}.tar.gz" - fi - - if [ -z "$ref" ]; then - echo -e "${RED}Error:${RESET} Unable to determine git reference from WP_TESTS_TAG: $WP_TESTS_TAG" - exit 1 - fi - - download "${archive_url}" "$TMPDIR/wordpress-develop.tar.gz" - - # Validate that the tarball was downloaded correctly before extracting - if [ ! -s "$TMPDIR/wordpress-develop.tar.gz" ]; then - echo -e "${RED}Error:${RESET} Downloaded test suite archive is missing or empty: $TMPDIR/wordpress-develop.tar.gz" - exit 1 - fi - - if ! tar -tzf "$TMPDIR/wordpress-develop.tar.gz" >/dev/null 2>&1; then - echo -e "${RED}Error:${RESET} Downloaded test suite archive is not a valid tar.gz file: $TMPDIR/wordpress-develop.tar.gz" - exit 1 - fi - - tar -zxmf "$TMPDIR/wordpress-develop.tar.gz" -C "$TMPDIR" - mv "$TMPDIR/wordpress-develop-${ref}/tests/phpunit/includes" "$WP_TESTS_DIR"/ - mv "$TMPDIR/wordpress-develop-${ref}/tests/phpunit/data" "$WP_TESTS_DIR"/ - rm -rf "$TMPDIR/wordpress-develop-${ref}" - rm "$TMPDIR/wordpress-develop.tar.gz" - echo -e "${GREEN}Test suite installed.${RESET}" - else - echo -e "${CYAN}Test suite is already installed.${RESET}" + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data fi - if [ ! -f "$WP_TESTS_DIR"/wp-tests-config.php ]; then - echo -e "${CYAN}Configuring test suite...${RESET}" - if [[ $WP_TESTS_TAG == 'trunk' ]]; then - ref=trunk - elif [[ $WP_TESTS_TAG == branches/* ]]; then - ref=${WP_TESTS_TAG#branches/} - else - ref=${WP_TESTS_TAG#tags/} - fi - - if [ -z "$ref" ]; then - echo -e "${RED}Error:${RESET} Unable to determine git reference from WP_TESTS_TAG: $WP_TESTS_TAG" - exit 1 - fi - - download https://raw.githubusercontent.com/WordPress/wordpress-develop/${ref}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php # remove all forward slashes in the end WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") - # escape special sed replacement characters in $WP_CORE_DIR (backslash, pipe, ampersand) - WP_CORE_DIR_ESCAPED=$(printf '%s' "$WP_CORE_DIR" | sed 's/[\\|&]/\\&/g') - sed $ioption "s|dirname( __FILE__ ) . '/src/'|'${WP_CORE_DIR_ESCAPED}/'|" "$WP_TESTS_DIR"/wp-tests-config.php - sed $ioption "s|__DIR__ . '/src/'|'${WP_CORE_DIR_ESCAPED}/'|" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php - echo -e "${GREEN}Test suite configured.${RESET}" - else - echo -e "${CYAN}Test suite is already configured.${RESET}" - fi - -} - -recreate_db() { - shopt -s nocasematch - if [[ $1 =~ ^(y|yes)$ ]] - then - echo -e "${CYAN}Recreating the database ($DB_NAME)...${RESET}" - if command -v mariadb-admin > /dev/null 2>&1; then - mariadb-admin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA - else - mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA - fi - create_db - echo -e "${GREEN}Database ($DB_NAME) recreated.${RESET}" - else - echo -e "${YELLOW}Leaving the existing database ($DB_NAME) in place.${RESET}" fi - shopt -u nocasematch -} -create_db() { - if command -v mariadb-admin > /dev/null 2>&1; then - mariadb-admin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA - else - mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA - fi } install_db() { if [ ${SKIP_DB_CREATE} = "true" ]; then - echo -e "${YELLOW}Skipping database creation.${RESET}" return 0 fi @@ -293,24 +144,9 @@ install_db() { fi # create database - if command -v mariadb > /dev/null 2>&1; then - local DB_CLIENT='mariadb' - else - local DB_CLIENT='mysql' - fi - if $DB_CLIENT --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep -q "^$DB_NAME$"; - then - echo -e "${YELLOW}Reinstalling will delete the existing test database ($DB_NAME)${RESET}" - read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB - recreate_db $DELETE_EXISTING_DB - else - echo -e "${CYAN}Creating database ($DB_NAME)...${RESET}" - create_db - echo -e "${GREEN}Database ($DB_NAME) created.${RESET}" - fi + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA } install_wp install_test_suite install_db -echo -e "${GREEN}Done.${RESET}" diff --git a/templates/phpcs.xml.dist b/templates/phpcs.xml.dist new file mode 100644 index 000000000..4f1d0e5cf --- /dev/null +++ b/templates/phpcs.xml.dist @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<ruleset name="WordPress Coding Standards for Plugins"> + <description>Generally-applicable sniffs for WordPress plugins</description> + + <rule ref="WordPress-Core" /> + <rule ref="WordPress-Docs" /> + + <!-- Check all PHP files in directory tree by default. --> + <arg name="extensions" value="php"/> + <file>.</file> + + <!-- Show progress and sniff codes in all reports --> + <arg value="ps"/> + + <exclude-pattern>*/node_modules/*</exclude-pattern> + <exclude-pattern>*/vendor/*</exclude-pattern> +</ruleset> diff --git a/templates/phpunit.xml.dist b/templates/phpunit.xml.dist index 165396799..d9af975f2 100644 --- a/templates/phpunit.xml.dist +++ b/templates/phpunit.xml.dist @@ -4,14 +4,12 @@ backupGlobals="false" colors="true" convertErrorsToExceptions="true" - convertWarningsToExceptions="true" convertNoticesToExceptions="true" - convertDeprecationsToExceptions="true" + convertWarningsToExceptions="true" > <testsuites> - <testsuite name="testing"> + <testsuite> <directory prefix="test-" suffix=".php">./tests/</directory> - <exclude>./tests/test-sample.php</exclude> </testsuite> </testsuites> </phpunit> diff --git a/templates/plugin-bitbucket.mustache b/templates/plugin-bitbucket.mustache deleted file mode 100644 index 827e2fa73..000000000 --- a/templates/plugin-bitbucket.mustache +++ /dev/null @@ -1,132 +0,0 @@ -pipelines: - default: - - step: - image: php:7.4 - name: "PHP 7.4" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - - # PHP extensions - - docker-php-ext-install -j$(nproc) mysqli pdo_mysql zip - - # Setup WordPress tests - - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - - # Install Composer - - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - - php composer-setup.php --install-dir=/usr/local/bin --filename=composer - - php -r "unlink('composer-setup.php');" - - export PATH="$PATH:$HOME/.composer/vendor/bin" - - export COMPOSER_ALLOW_SUPERUSER=1 - - # Install PHPUnit - - PHPUNIT_VERSION=9.6.19 - - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - - composer global require yoast/phpunit-polyfills - - export WP_TESTS_PHPUNIT_POLYFILLS_PATH="$HOME/.composer/vendor/yoast/phpunit-polyfills" - - phpunit --version - - # Install PHPCS and WPCS - - composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true - - composer global require "wp-coding-standards/wpcs" - - composer global require "phpcompatibility/phpcompatibility-wp" - - phpcs --version - - # Run PHPCS - - phpcs - - # Run PHPUnit - - phpunit - services: - - database - - - step: - image: php:8.0 - name: "PHP 8.0" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - - # PHP extensions - - docker-php-ext-install -j$(nproc) mysqli pdo_mysql zip - - # Setup WordPress tests - - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - - # Install Composer - - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - - php composer-setup.php --install-dir=/usr/local/bin --filename=composer - - php -r "unlink('composer-setup.php');" - - export PATH="$PATH:$HOME/.composer/vendor/bin" - - export COMPOSER_ALLOW_SUPERUSER=1 - - # Install PHPUnit - - PHPUNIT_VERSION=9.6.19 - - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - - composer global require yoast/phpunit-polyfills - - export WP_TESTS_PHPUNIT_POLYFILLS_PATH="$HOME/.composer/vendor/yoast/phpunit-polyfills" - - phpunit --version - - # Install PHPCS and WPCS - - composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true - - composer global require "wp-coding-standards/wpcs" - - composer global require "phpcompatibility/phpcompatibility-wp" - - phpcs --version - - # Run PHPCS - - phpcs - - # Run PHPUnit - - phpunit - services: - - database - - - step: - image: php:8.2 - name: "PHP 8.2" - script: - # Install Dependencies - - apt-get update && apt-get install -y subversion git zip libzip-dev --no-install-recommends - - # PHP extensions - - docker-php-ext-install -j$(nproc) mysqli pdo_mysql zip - - # Setup WordPress tests - - bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - - # Install Composer - - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - - php composer-setup.php --install-dir=/usr/local/bin --filename=composer - - php -r "unlink('composer-setup.php');" - - export PATH="$PATH:$HOME/.composer/vendor/bin" - - export COMPOSER_ALLOW_SUPERUSER=1 - - # Install PHPUnit - - PHPUNIT_VERSION=9.6.19 - - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - - composer global require yoast/phpunit-polyfills - - export WP_TESTS_PHPUNIT_POLYFILLS_PATH="$HOME/.composer/vendor/yoast/phpunit-polyfills" - - phpunit --version - - # Install PHPCS and WPCS - - composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true - - composer global require "wp-coding-standards/wpcs" - - composer global require "phpcompatibility/phpcompatibility-wp" - - phpcs --version - - # Run PHPCS - - phpcs - - # Run PHPUnit - - phpunit - services: - - database - -definitions: - services: - database: - image: mysql:latest - environment: - MYSQL_DATABASE: 'wordpress_tests' - MYSQL_ROOT_PASSWORD: 'root' diff --git a/templates/plugin-bootstrap.mustache b/templates/plugin-bootstrap.mustache index 0b21e2e91..c0344b343 100644 --- a/templates/plugin-bootstrap.mustache +++ b/templates/plugin-bootstrap.mustache @@ -1,6 +1,6 @@ <?php /** - * PHPUnit bootstrap file. + * PHPUnit bootstrap file * * @package {{plugin_package}} */ @@ -11,28 +11,21 @@ if ( ! $_tests_dir ) { $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; } -// Forward custom PHPUnit Polyfills configuration to PHPUnit bootstrap file. -$_phpunit_polyfills_path = getenv( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH' ); -if ( false !== $_phpunit_polyfills_path ) { - define( 'WP_TESTS_PHPUNIT_POLYFILLS_PATH', $_phpunit_polyfills_path ); -} - -if ( ! file_exists( "{$_tests_dir}/includes/functions.php" ) ) { - echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped +if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { + echo "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; exit( 1 ); } // Give access to tests_add_filter() function. -require_once "{$_tests_dir}/includes/functions.php"; +require_once $_tests_dir . '/includes/functions.php'; /** * Manually load the plugin being tested. */ function _manually_load_plugin() { - require dirname( __DIR__ ) . '/{{plugin_main_file}}'; + require dirname( dirname( __FILE__ ) ) . '/{{plugin_slug}}.php'; } - tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); // Start up the WP testing environment. -require "{$_tests_dir}/includes/bootstrap.php"; +require $_tests_dir . '/includes/bootstrap.php'; diff --git a/templates/plugin-circle.mustache b/templates/plugin-circle.mustache index 6f16d526c..b0de043e9 100644 --- a/templates/plugin-circle.mustache +++ b/templates/plugin-circle.mustache @@ -5,38 +5,30 @@ workflows: - php56-build - php70-build - php71-build - - php72-build - - php73-build - - php74-build version: 2 - -job-references: - mysql_image: &mysql_image - circleci/mysql:5.6 - - setup_environment: &setup_environment - name: "Setup Environment Variables" - command: | - echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV - source /home/circleci/.bashrc - - install_dependencies: &install_dependencies - name: "Install Dependencies" - command: | - sudo apt-get update && sudo apt-get install subversion - sudo -E docker-php-ext-install mysqli - sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" - sudo apt-get update && sudo apt-get install mysql-client-5.7 - - php_job: &php_job +jobs: + php56-build: + docker: + - image: circleci/php:5.6 + - image: circleci/mysql:5.7 environment: - WP_TESTS_DIR: "/tmp/wordpress-tests-lib" - WP_CORE_DIR: "/tmp/wordpress/" steps: - checkout - - run: *setup_environment - - run: *install_dependencies + - run: + name: "Setup Environment Variables" + command: | + echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV + source /home/circleci/.bashrc + - run: + name: "Install Dependencies" + command: | + sudo apt-get update && sudo apt-get install subversion + sudo docker-php-ext-install mysqli + sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" + sudo apt-get update && sudo apt-get install mysql-client-5.7 - run: name: "Run Tests" command: | @@ -44,21 +36,36 @@ job-references: composer global require wp-coding-standards/wpcs phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs phpcs + SKIP_DB_CREATE=false + {{#wp_versions_to_test}} rm -rf $WP_TESTS_DIR $WP_CORE_DIR - bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 {{.}} $SKIP_DB_CREATE phpunit WP_MULTISITE=1 phpunit + SKIP_DB_CREATE=true + {{/wp_versions_to_test}} -jobs: - php56-build: - <<: *php_job + php70-build: docker: - - image: circleci/php:5.6 - - image: *mysql_image + - image: circleci/php:7.0 + - image: circleci/mysql:5.7 + environment: + - WP_TESTS_DIR: "/tmp/wordpress-tests-lib" + - WP_CORE_DIR: "/tmp/wordpress/" steps: - checkout - - run: *setup_environment - - run: *install_dependencies + - run: + name: "Setup Environment Variables" + command: | + echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV + source /home/circleci/.bashrc + - run: + name: "Install Dependencies" + command: | + sudo apt-get update && sudo apt-get install subversion + sudo docker-php-ext-install mysqli + sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" + sudo apt-get update && sudo apt-get install mysql-client-5.7 - run: name: "Run Tests" command: | @@ -66,41 +73,40 @@ jobs: composer global require wp-coding-standards/wpcs phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs phpcs - SKIP_DB_CREATE=false - {{#wp_versions_to_test}} rm -rf $WP_TESTS_DIR $WP_CORE_DIR - bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 {{.}} $SKIP_DB_CREATE + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest phpunit WP_MULTISITE=1 phpunit - SKIP_DB_CREATE=true - {{/wp_versions_to_test}} - - php70-build: - <<: *php_job - docker: - - image: circleci/php:7.0 - - image: *mysql_image php71-build: - <<: *php_job docker: - image: circleci/php:7.1 - - image: *mysql_image - - php72-build: - <<: *php_job - docker: - - image: circleci/php:7.2 - - image: *mysql_image - - php73-build: - <<: *php_job - docker: - - image: circleci/php:7.3 - - image: *mysql_image - - php74-build: - <<: *php_job - docker: - - image: circleci/php:7.4 - - image: *mysql_image + - image: circleci/mysql:5.7 + environment: + - WP_TESTS_DIR: "/tmp/wordpress-tests-lib" + - WP_CORE_DIR: "/tmp/wordpress/" + steps: + - checkout + - run: + name: "Setup Environment Variables" + command: | + echo "export PATH=$HOME/.composer/vendor/bin:$PATH" >> $BASH_ENV + source /home/circleci/.bashrc + - run: + name: "Install Dependencies" + command: | + sudo apt-get update && sudo apt-get install subversion + sudo docker-php-ext-install mysqli + sudo sh -c "printf '\ndeb http://ftp.us.debian.org/debian sid main\n' >> /etc/apt/sources.list" + sudo apt-get update && sudo apt-get install mysql-client-5.7 + - run: + name: "Run Tests" + command: | + composer global require "phpunit/phpunit=5.7.*" + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + phpcs + rm -rf $WP_TESTS_DIR $WP_CORE_DIR + bash bin/install-wp-tests.sh wordpress_test root '' 127.0.0.1 latest + phpunit + WP_MULTISITE=1 phpunit diff --git a/templates/plugin-composer.mustache b/templates/plugin-composer.mustache deleted file mode 100644 index aa3ad7147..000000000 --- a/templates/plugin-composer.mustache +++ /dev/null @@ -1,8 +0,0 @@ -{ - "require-dev": { - "wp-cli/i18n-command": "^2" - }, - "scripts": { - "makepot": "wp i18n make-pot ." - } -} diff --git a/templates/plugin-distignore.mustache b/templates/plugin-distignore.mustache index af58f8c7b..971fec40c 100644 --- a/templates/plugin-distignore.mustache +++ b/templates/plugin-distignore.mustache @@ -1,25 +1,17 @@ # A set of files you probably don't want in your WordPress.org distribution -.babelrc -.deployignore .distignore .editorconfig -.eslintignore -.eslintrc .git .gitignore -.github .gitlab-ci.yml .travis.yml .DS_Store -.*~ Thumbs.db behat.yml -bitbucket-pipelines.yml bin .circleci/config.yml composer.json composer.lock -dependencies.yml Gruntfile.js package.json package-lock.json @@ -27,12 +19,9 @@ phpunit.xml phpunit.xml.dist multisite.xml multisite.xml.dist -.phpcs.xml phpcs.xml -.phpcs.xml.dist phpcs.xml.dist README.md -webpack.config.js wp-cli.local.yml yarn.lock tests diff --git a/templates/plugin-github.mustache b/templates/plugin-github.mustache deleted file mode 100644 index f8662591d..000000000 --- a/templates/plugin-github.mustache +++ /dev/null @@ -1,42 +0,0 @@ -{{=<% %>=}} -name: Testing - -on: - pull_request: - branches: - - main - - master - -jobs: - phpunit: - name: Run tests - runs-on: ubuntu-latest - strategy: - matrix: - php-version: ['8.2', '8.0', '7.4'] - services: - database: - image: mysql:latest - env: - MYSQL_DATABASE: wordpress_tests - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - steps: - - name: Check out source code - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - - - name: Setup PHP - uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 - with: - php-version: ${{ matrix.php-version }} - tools: phpunit-polyfills:1.1 - - - name: Install SVN - run: sudo apt-get update && sudo apt-get install -y subversion - - - name: Setup tests - run: bash bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1 latest true - - - name: Run tests - run: phpunit diff --git a/templates/plugin-gitignore.mustache b/templates/plugin-gitignore.mustache index 6cc91535c..6f68bc7f6 100644 --- a/templates/plugin-gitignore.mustache +++ b/templates/plugin-gitignore.mustache @@ -4,7 +4,6 @@ phpunit.xml Thumbs.db wp-cli.local.yml node_modules/ -vendor/ *.sql *.tar.gz *.zip diff --git a/templates/plugin-gitlab.mustache b/templates/plugin-gitlab.mustache index 5d6871eb0..6c7ca7ecc 100644 --- a/templates/plugin-gitlab.mustache +++ b/templates/plugin-gitlab.mustache @@ -5,56 +5,53 @@ variables: before_script: # Install dependencies - - # Update the docker + + # update the docker + - apt-get clean - apt-get -yqq update - - apt-get -yqqf install zip unzip subversion default-mysql-client default-libmysqlclient-dev --fix-missing + + # instll the required packages for the running CI tests + - apt-get -yqqf install zip unzip subversion mysql-client libmysqlclient-dev --fix-missing # PHP extensions - - docker-php-ext-install -j$(nproc) mysqli pdo_mysql + - docker-php-ext-enable mbstring mcrypt mysqli pdo_mysql intl gd zip bz2 - # Setup WordPress tests + # Set up WordPress tests - bash bin/install-wp-tests.sh wordpress_tests root mysql mysql latest true - # Install Composer - - php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" - - php composer-setup.php --install-dir=/usr/local/bin --filename=composer - - php -r "unlink('composer-setup.php');" - - export PATH="$PATH:$HOME/.composer/vendor/bin" - - # Install PHPUnit - - PHPUNIT_VERSION=9.6.19 - - curl -o /usr/local/bin/phpunit "https://phar.phpunit.de/phpunit-${PHPUNIT_VERSION}.phar" && chmod +x /usr/local/bin/phpunit - - composer global require yoast/phpunit-polyfills - - export WP_TESTS_PHPUNIT_POLYFILLS_PATH="$HOME/.composer/vendor/yoast/phpunit-polyfills" - - phpunit --version - # Install PHPCS and WPCS - - composer global config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + - composer global require "squizlabs/php_codesniffer=*" - composer global require "wp-coding-standards/wpcs" - - composer global require "phpcompatibility/phpcompatibility-wp" - - phpcs --version + - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs -PHPunit:PHP7.4:MySQL: - image: php:7.4-bullseye +PHPunit:PHP5.3:MySQL: + image: tetraweb/php:5.3 services: - - mysql:5.7 + - mysql:5.6 script: - phpcs - phpunit -PHPunit:PHP8.0:MySQL: - image: php:8.0-bullseye +PHPunit:PHP5.6:MySQL: + image: tetraweb/php:5.6 + services: + - mysql:5.6 + script: + - phpcs + - phpunit + +PHPunit:PHP7.0:MySQL: + image: tetraweb/php:7.0 services: - - mysql:5.7 + - mysql:5.6 script: - phpcs - phpunit -PHPunit:PHP8.2:MySQL: - image: php:8.2-bullseye +PHPunit:PHP7.1:MySQL: + image: tetraweb/php:7.1 services: - - mysql:5.7 + - mysql:5.6 script: - phpcs - phpunit diff --git a/templates/plugin-gruntfile.mustache b/templates/plugin-gruntfile.mustache new file mode 100644 index 000000000..eda2254e6 --- /dev/null +++ b/templates/plugin-gruntfile.mustache @@ -0,0 +1,56 @@ +module.exports = function( grunt ) { + + 'use strict'; + + // Project configuration + grunt.initConfig( { + + pkg: grunt.file.readJSON( 'package.json' ), + + addtextdomain: { + options: { + textdomain: '{{textdomain}}', + }, + update_all_domains: { + options: { + updateDomains: true + }, + src: [ '*.php', '**/*.php', '!\.git/**/*', '!bin/**/*', '!node_modules/**/*', '!tests/**/*' ] + } + }, + + wp_readme_to_markdown: { + your_target: { + files: { + 'README.md': 'readme.txt' + } + }, + }, + + makepot: { + target: { + options: { + domainPath: '/languages', + exclude: [ '\.git/*', 'bin/*', 'node_modules/*', 'tests/*' ], + mainFile: '{{plugin_slug}}.php', + potFilename: '{{plugin_slug}}.pot', + potHeaders: { + poedit: true, + 'x-poedit-keywordslist': true + }, + type: 'wp-plugin', + updateTimestamp: true + } + } + }, + } ); + + grunt.loadNpmTasks( 'grunt-wp-i18n' ); + grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' ); + grunt.registerTask( 'default', [ 'i18n','readme' ] ); + grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] ); + grunt.registerTask( 'readme', ['wp_readme_to_markdown'] ); + + grunt.util.linefeed = '\n'; + +}; diff --git a/templates/plugin-packages.mustache b/templates/plugin-packages.mustache new file mode 100644 index 000000000..84a60ca66 --- /dev/null +++ b/templates/plugin-packages.mustache @@ -0,0 +1,12 @@ + +{ + "name": "{{plugin_slug}}", + "version": "0.1.0", + "main": "Gruntfile.js", + "author": "{{plugin_author}}", + "devDependencies": { + "grunt": "~0.4.5", + "grunt-wp-i18n": "~0.5.0", + "grunt-wp-readme-to-markdown": "~1.0.0" + } +} diff --git a/templates/plugin-readme.mustache b/templates/plugin-readme.mustache index c2392661b..dc9fb00df 100644 --- a/templates/plugin-readme.mustache +++ b/templates/plugin-readme.mustache @@ -4,7 +4,6 @@ Donate link: https://example.com/ Tags: comments, spam Requires at least: 4.5 Tested up to: {{plugin_tested_up_to}} -Requires PHP: 5.6 Stable tag: 0.1.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/templates/plugin-test-sample.mustache b/templates/plugin-test-sample.mustache index 1fe7bf4f7..2e897d13d 100644 --- a/templates/plugin-test-sample.mustache +++ b/templates/plugin-test-sample.mustache @@ -13,7 +13,7 @@ class SampleTest extends WP_UnitTestCase { /** * A single example test. */ - public function test_sample() { + function test_sample() { // Replace this with some actual testing code. $this->assertTrue( true ); } diff --git a/templates/plugin-travis.mustache b/templates/plugin-travis.mustache new file mode 100644 index 000000000..0825f2930 --- /dev/null +++ b/templates/plugin-travis.mustache @@ -0,0 +1,63 @@ +sudo: false +dist: trusty + +language: php + +notifications: + email: + on_success: never + on_failure: change + +branches: + only: + - master + +cache: + directories: + - $HOME/.composer/cache + +matrix: + include: + - php: 7.1 + env: WP_VERSION=latest + - php: 7.0 + env: WP_VERSION=latest + {{#wp_versions_to_test}} + - php: 5.6 + env: WP_VERSION={{.}} + {{/wp_versions_to_test}} + - php: 5.6 + env: WP_TRAVISCI=phpcs + - php: 5.3 + env: WP_VERSION=latest + dist: precise + +before_script: + - export PATH="$HOME/.composer/vendor/bin:$PATH" + - | + 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 + - | + if [[ ! -z "$WP_VERSION" ]] ; then + bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION + composer global require "phpunit/phpunit=4.8.*|5.7.*" + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + composer global require wp-coding-standards/wpcs + phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs + fi + +script: + - | + if [[ ! -z "$WP_VERSION" ]] ; then + phpunit + WP_MULTISITE=1 phpunit + fi + - | + if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then + phpcs + fi diff --git a/templates/plugin.mustache b/templates/plugin.mustache index 097fc41e6..1f6190bda 100644 --- a/templates/plugin.mustache +++ b/templates/plugin.mustache @@ -13,6 +13,3 @@ */ // Your code starts here. -if ( ! defined( 'ABSPATH' ) ) { - exit; -} diff --git a/templates/post_type.mustache b/templates/post_type.mustache index 7115162a6..e58c0ff37 100644 --- a/templates/post_type.mustache +++ b/templates/post_type.mustache @@ -1,45 +1,41 @@ - register_post_type( - '{{slug}}', - array( - 'labels' => array( - 'name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), - 'singular_name' => __( '{{label_ucfirst}}', '{{textdomain}}' ), - 'all_items' => __( 'All {{label_plural_ucfirst}}', '{{textdomain}}' ), - 'archives' => __( '{{label_ucfirst}} Archives', '{{textdomain}}' ), - 'attributes' => __( '{{label_ucfirst}} Attributes', '{{textdomain}}' ), - 'insert_into_item' => __( 'Insert into {{label}}', '{{textdomain}}' ), - 'uploaded_to_this_item' => __( 'Uploaded to this {{label}}', '{{textdomain}}' ), - 'featured_image' => _x( 'Featured Image', '{{slug}}', '{{textdomain}}' ), - 'set_featured_image' => _x( 'Set featured image', '{{slug}}', '{{textdomain}}' ), - 'remove_featured_image' => _x( 'Remove featured image', '{{slug}}', '{{textdomain}}' ), - 'use_featured_image' => _x( 'Use as featured image', '{{slug}}', '{{textdomain}}' ), - 'filter_items_list' => __( 'Filter {{label_plural}} list', '{{textdomain}}' ), - 'items_list_navigation' => __( '{{label_plural_ucfirst}} list navigation', '{{textdomain}}' ), - 'items_list' => __( '{{label_plural_ucfirst}} list', '{{textdomain}}' ), - 'new_item' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), - 'add_new' => __( 'Add New', '{{textdomain}}' ), - 'add_new_item' => __( 'Add New {{label_ucfirst}}', '{{textdomain}}' ), - 'edit_item' => __( 'Edit {{label_ucfirst}}', '{{textdomain}}' ), - 'view_item' => __( 'View {{label_ucfirst}}', '{{textdomain}}' ), - 'view_items' => __( 'View {{label_plural_ucfirst}}', '{{textdomain}}' ), - 'search_items' => __( 'Search {{label_plural}}', '{{textdomain}}' ), - 'not_found' => __( 'No {{label_plural}} found', '{{textdomain}}' ), - 'not_found_in_trash' => __( 'No {{label_plural}} found in trash', '{{textdomain}}' ), - 'parent_item_colon' => __( 'Parent {{label_ucfirst}}:', '{{textdomain}}' ), - 'menu_name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), - ), - 'public' => true, - 'hierarchical' => false, - 'show_ui' => true, - 'show_in_nav_menus' => true, - 'supports' => array( 'title', 'editor' ), - 'has_archive' => true, - 'rewrite' => true, - 'query_var' => true, - 'menu_position' => null, - 'menu_icon' => 'dashicons-{{dashicon}}', - 'show_in_rest' => true, - 'rest_base' => '{{slug}}', - 'rest_controller_class' => 'WP_REST_Posts_Controller', - ) - ); + register_post_type( '{{slug}}', array( + 'labels' => array( + 'name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), + 'singular_name' => __( '{{label_ucfirst}}', '{{textdomain}}' ), + 'all_items' => __( 'All {{label_plural_ucfirst}}', '{{textdomain}}' ), + 'archives' => __( '{{label_ucfirst}} Archives', '{{textdomain}}' ), + 'attributes' => __( '{{label_ucfirst}} Attributes', '{{textdomain}}' ), + 'insert_into_item' => __( 'Insert into {{label}}', '{{textdomain}}' ), + 'uploaded_to_this_item' => __( 'Uploaded to this {{label}}', '{{textdomain}}' ), + 'featured_image' => _x( 'Featured Image', '{{slug}}', '{{textdomain}}' ), + 'set_featured_image' => _x( 'Set featured image', '{{slug}}', '{{textdomain}}' ), + 'remove_featured_image' => _x( 'Remove featured image', '{{slug}}', '{{textdomain}}' ), + 'use_featured_image' => _x( 'Use as featured image', '{{slug}}', '{{textdomain}}' ), + 'filter_items_list' => __( 'Filter {{label_plural}} list', '{{textdomain}}' ), + 'items_list_navigation' => __( '{{label_plural_ucfirst}} list navigation', '{{textdomain}}' ), + 'items_list' => __( '{{label_plural_ucfirst}} list', '{{textdomain}}' ), + 'new_item' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), + 'add_new' => __( 'Add New', '{{textdomain}}' ), + 'add_new_item' => __( 'Add New {{label_ucfirst}}', '{{textdomain}}' ), + 'edit_item' => __( 'Edit {{label_ucfirst}}', '{{textdomain}}' ), + 'view_item' => __( 'View {{label_ucfirst}}', '{{textdomain}}' ), + 'view_items' => __( 'View {{label_plural_ucfirst}}', '{{textdomain}}' ), + 'search_items' => __( 'Search {{label_plural}}', '{{textdomain}}' ), + 'not_found' => __( 'No {{label_plural}} found', '{{textdomain}}' ), + 'not_found_in_trash' => __( 'No {{label_plural}} found in trash', '{{textdomain}}' ), + 'parent_item_colon' => __( 'Parent {{label_ucfirst}}:', '{{textdomain}}' ), + 'menu_name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), + ), + 'public' => true, + 'hierarchical' => false, + 'show_ui' => true, + 'show_in_nav_menus' => true, + 'supports' => array( 'title', 'editor' ), + 'has_archive' => true, + 'rewrite' => true, + 'query_var' => true, + 'menu_icon' => 'dashicons-{{dashicon}}', + 'show_in_rest' => true, + 'rest_base' => '{{slug}}', + 'rest_controller_class' => 'WP_REST_Posts_Controller', + ) ); diff --git a/templates/post_type_extended.mustache b/templates/post_type_extended.mustache index 288fa1c9d..8c0a1fd1f 100644 --- a/templates/post_type_extended.mustache +++ b/templates/post_type_extended.mustache @@ -1,18 +1,12 @@ <?php -/** - * Custom post type - * - * @package {{prefix}} - */ /** * Registers the `{{machine_name}}` post type. */ -function {{prefix}}_init() { +function {{machine_name}}_init() { {{output}} } - -add_action( 'init', '{{prefix}}_init' ); +add_action( 'init', '{{machine_name}}_init' ); /** * Sets the post updated messages for the `{{machine_name}}` post type. @@ -20,7 +14,7 @@ add_action( 'init', '{{prefix}}_init' ); * @param array $messages Post updated messages. * @return array Messages for the `{{machine_name}}` post type. */ -function {{prefix}}_updated_messages( $messages ) { +function {{machine_name}}_updated_messages( $messages ) { global $post; $permalink = get_permalink( $post ); @@ -33,49 +27,19 @@ function {{prefix}}_updated_messages( $messages ) { 3 => __( 'Custom field deleted.', '{{textdomain}}' ), 4 => __( '{{label_ucfirst}} updated.', '{{textdomain}}' ), /* translators: %s: date and time of the revision */ - 5 => isset( $_GET['revision'] ) ? sprintf( __( '{{label_ucfirst}} restored to revision from %s', '{{textdomain}}' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, // phpcs:ignore WordPress.Security.NonceVerification.Recommended + 5 => isset( $_GET['revision'] ) ? sprintf( __( '{{label_ucfirst}} restored to revision from %s', '{{textdomain}}' ), wp_post_revision_title( (int) $_GET['revision'], false ) ) : false, /* translators: %s: post permalink */ 6 => sprintf( __( '{{label_ucfirst}} published. <a href="%s">View {{label}}</a>', '{{textdomain}}' ), esc_url( $permalink ) ), 7 => __( '{{label_ucfirst}} saved.', '{{textdomain}}' ), /* translators: %s: post permalink */ 8 => sprintf( __( '{{label_ucfirst}} submitted. <a target="_blank" href="%s">Preview {{label}}</a>', '{{textdomain}}' ), esc_url( add_query_arg( 'preview', 'true', $permalink ) ) ), /* translators: 1: Publish box date format, see https://secure.php.net/date 2: Post permalink */ - 9 => sprintf( __( '{{label_ucfirst}} scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview {{label}}</a>', '{{textdomain}}' ), date_i18n( __( 'M j, Y @ G:i', '{{textdomain}}' ), strtotime( $post->post_date ) ), esc_url( $permalink ) ), + 9 => sprintf( __( '{{label_ucfirst}} scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview {{label}}</a>', '{{textdomain}}' ), + date_i18n( __( 'M j, Y @ G:i' ), strtotime( $post->post_date ) ), esc_url( $permalink ) ), /* translators: %s: post permalink */ 10 => sprintf( __( '{{label_ucfirst}} draft updated. <a target="_blank" href="%s">Preview {{label}}</a>', '{{textdomain}}' ), esc_url( add_query_arg( 'preview', 'true', $permalink ) ) ), ); return $messages; } - -add_filter( 'post_updated_messages', '{{prefix}}_updated_messages' ); - -/** - * Sets the bulk post updated messages for the `{{machine_name}}` post type. - * - * @param array $bulk_messages Arrays of messages, each keyed by the corresponding post type. Messages are - * keyed with 'updated', 'locked', 'deleted', 'trashed', and 'untrashed'. - * @param int[] $bulk_counts Array of item counts for each message, used to build internationalized strings. - * @return array Bulk messages for the `{{machine_name}}` post type. - */ -function {{prefix}}_bulk_updated_messages( $bulk_messages, $bulk_counts ) { - global $post; - - $bulk_messages['{{slug}}'] = array( - /* translators: %s: Number of {{label_plural}}. */ - 'updated' => _n( '%s {{label}} updated.', '%s {{label_plural}} updated.', $bulk_counts['updated'], '{{textdomain}}' ), - 'locked' => ( 1 === $bulk_counts['locked'] ) ? __( '1 {{label}} not updated, somebody is editing it.', '{{textdomain}}' ) : - /* translators: %s: Number of {{label_plural}}. */ - _n( '%s {{label}} not updated, somebody is editing it.', '%s {{label_plural}} not updated, somebody is editing them.', $bulk_counts['locked'], '{{textdomain}}' ), - /* translators: %s: Number of {{label_plural}}. */ - 'deleted' => _n( '%s {{label}} permanently deleted.', '%s {{label_plural}} permanently deleted.', $bulk_counts['deleted'], '{{textdomain}}' ), - /* translators: %s: Number of {{label_plural}}. */ - 'trashed' => _n( '%s {{label}} moved to the Trash.', '%s {{label_plural}} moved to the Trash.', $bulk_counts['trashed'], '{{textdomain}}' ), - /* translators: %s: Number of {{label_plural}}. */ - 'untrashed' => _n( '%s {{label}} restored from the Trash.', '%s {{label_plural}} restored from the Trash.', $bulk_counts['untrashed'], '{{textdomain}}' ), - ); - - return $bulk_messages; -} - -add_filter( 'bulk_post_updated_messages', '{{prefix}}_bulk_updated_messages', 10, 2 ); +add_filter( 'post_updated_messages', '{{machine_name}}_updated_messages' ); diff --git a/templates/taxonomy.mustache b/templates/taxonomy.mustache index 6f5ea8958..a3367a4c2 100644 --- a/templates/taxonomy.mustache +++ b/templates/taxonomy.mustache @@ -1,46 +1,42 @@ - register_taxonomy( - '{{slug}}', - array( {{post_types}} ), - array( - 'hierarchical' => false, - 'public' => true, - 'show_in_nav_menus' => true, - 'show_ui' => true, - 'show_admin_column' => false, - 'query_var' => true, - 'rewrite' => true, - 'capabilities' => array( - 'manage_terms' => 'edit_posts', - 'edit_terms' => 'edit_posts', - 'delete_terms' => 'edit_posts', - 'assign_terms' => 'edit_posts', - ), - 'labels' => array( - 'name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), - 'singular_name' => _x( '{{label_ucfirst}}', 'taxonomy general name', '{{textdomain}}' ), - 'search_items' => __( 'Search {{label_plural_ucfirst}}', '{{textdomain}}' ), - 'popular_items' => __( 'Popular {{label_plural_ucfirst}}', '{{textdomain}}' ), - 'all_items' => __( 'All {{label_plural_ucfirst}}', '{{textdomain}}' ), - 'parent_item' => __( 'Parent {{label_ucfirst}}', '{{textdomain}}' ), - 'parent_item_colon' => __( 'Parent {{label_ucfirst}}:', '{{textdomain}}' ), - 'edit_item' => __( 'Edit {{label_ucfirst}}', '{{textdomain}}' ), - 'update_item' => __( 'Update {{label_ucfirst}}', '{{textdomain}}' ), - 'view_item' => __( 'View {{label_ucfirst}}', '{{textdomain}}' ), - 'add_new_item' => __( 'Add New {{label_ucfirst}}', '{{textdomain}}' ), - 'new_item_name' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), - 'separate_items_with_commas' => __( 'Separate {{label_plural}} with commas', '{{textdomain}}' ), - 'add_or_remove_items' => __( 'Add or remove {{label_plural}}', '{{textdomain}}' ), - 'choose_from_most_used' => __( 'Choose from the most used {{label_plural}}', '{{textdomain}}' ), - 'not_found' => __( 'No {{label_plural}} found.', '{{textdomain}}' ), - 'no_terms' => __( 'No {{label_plural}}', '{{textdomain}}' ), - 'menu_name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), - 'items_list_navigation' => __( '{{label_plural_ucfirst}} list navigation', '{{textdomain}}' ), - 'items_list' => __( '{{label_plural_ucfirst}} list', '{{textdomain}}' ), - 'most_used' => _x( 'Most Used', '{{slug}}', '{{textdomain}}' ), - 'back_to_items' => __( '← Back to {{label_plural_ucfirst}}', '{{textdomain}}' ), - ), - 'show_in_rest' => true, - 'rest_base' => '{{slug}}', - 'rest_controller_class' => 'WP_REST_Terms_Controller', - ) - ); + register_taxonomy( '{{slug}}', array( {{post_types}} ), array( + 'hierarchical' => false, + 'public' => true, + 'show_in_nav_menus' => true, + 'show_ui' => true, + 'show_admin_column' => false, + 'query_var' => true, + 'rewrite' => true, + 'capabilities' => array( + 'manage_terms' => 'edit_posts', + 'edit_terms' => 'edit_posts', + 'delete_terms' => 'edit_posts', + 'assign_terms' => 'edit_posts', + ), + 'labels' => array( + 'name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), + 'singular_name' => _x( '{{label_ucfirst}}', 'taxonomy general name', '{{textdomain}}' ), + 'search_items' => __( 'Search {{label_plural_ucfirst}}', '{{textdomain}}' ), + 'popular_items' => __( 'Popular {{label_plural_ucfirst}}', '{{textdomain}}' ), + 'all_items' => __( 'All {{label_plural_ucfirst}}', '{{textdomain}}' ), + 'parent_item' => __( 'Parent {{label_ucfirst}}', '{{textdomain}}' ), + 'parent_item_colon' => __( 'Parent {{label_ucfirst}}:', '{{textdomain}}' ), + 'edit_item' => __( 'Edit {{label_ucfirst}}', '{{textdomain}}' ), + 'update_item' => __( 'Update {{label_ucfirst}}', '{{textdomain}}' ), + 'view_item' => __( 'View {{label_ucfirst}}', '{{textdomain}}' ), + 'add_new_item' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), + 'new_item_name' => __( 'New {{label_ucfirst}}', '{{textdomain}}' ), + 'separate_items_with_commas' => __( 'Separate {{label_plural}} with commas', '{{textdomain}}' ), + 'add_or_remove_items' => __( 'Add or remove {{label_plural}}', '{{textdomain}}' ), + 'choose_from_most_used' => __( 'Choose from the most used {{label_plural}}', '{{textdomain}}' ), + 'not_found' => __( 'No {{label_plural}} found.', '{{textdomain}}' ), + 'no_terms' => __( 'No {{label_plural}}', '{{textdomain}}' ), + 'menu_name' => __( '{{label_plural_ucfirst}}', '{{textdomain}}' ), + 'items_list_navigation' => __( '{{label_plural_ucfirst}} list navigation', '{{textdomain}}' ), + 'items_list' => __( '{{label_plural_ucfirst}} list', '{{textdomain}}' ), + 'most_used' => _x( 'Most Used', '{{slug}}', '{{textdomain}}' ), + 'back_to_items' => __( '← Back to {{label_plural_ucfirst}}', '{{textdomain}}' ), + ), + 'show_in_rest' => true, + 'rest_base' => '{{slug}}', + 'rest_controller_class' => 'WP_REST_Terms_Controller', + ) ); diff --git a/templates/taxonomy_extended.mustache b/templates/taxonomy_extended.mustache index fc71a6596..353560b8c 100644 --- a/templates/taxonomy_extended.mustache +++ b/templates/taxonomy_extended.mustache @@ -1,19 +1,13 @@ <?php -/** - * Custom taxonomy - * - * @package {{prefix}} - */ /** * Registers the `{{machine_name}}` taxonomy, * for use with {{post_types}}. */ -function {{prefix}}_init() { +function {{machine_name}}_init() { {{output}} } - -add_action( 'init', '{{prefix}}_init' ); +add_action( 'init', '{{machine_name}}_init' ); /** * Sets the post updated messages for the `{{machine_name}}` taxonomy. @@ -21,7 +15,7 @@ add_action( 'init', '{{prefix}}_init' ); * @param array $messages Post updated messages. * @return array Messages for the `{{machine_name}}` taxonomy. */ -function {{prefix}}_updated_messages( $messages ) { +function {{machine_name}}_updated_messages( $messages ) { $messages['{{slug}}'] = array( 0 => '', // Unused. Messages start at index 1. @@ -35,5 +29,4 @@ function {{prefix}}_updated_messages( $messages ) { return $messages; } - -add_filter( 'term_updated_messages', '{{prefix}}_updated_messages' ); +add_filter( 'term_updated_messages', '{{machine_name}}_updated_messages' ); diff --git a/templates/theme-bootstrap.mustache b/templates/theme-bootstrap.mustache index 04002b08c..d320dcb44 100644 --- a/templates/theme-bootstrap.mustache +++ b/templates/theme-bootstrap.mustache @@ -1,49 +1,46 @@ <?php /** - * PHPUnit bootstrap file. + * PHPUnit bootstrap file * * @package {{theme_package}} */ $_tests_dir = getenv( 'WP_TESTS_DIR' ); - if ( ! $_tests_dir ) { - $_tests_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; + $_tests_dir = '/tmp/wordpress-tests-lib'; } if ( ! file_exists( $_tests_dir . '/includes/functions.php' ) ) { - echo "Could not find {$_tests_dir}/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; - exit( 1 ); + throw new Exception( "Could not find $_tests_dir/includes/functions.php, have you run bin/install-wp-tests.sh ?" ); } // Give access to tests_add_filter() function. -require_once "{$_tests_dir}/includes/functions.php"; +require_once $_tests_dir . '/includes/functions.php'; /** - * Registers theme. + * Registers theme */ function _register_theme() { - $theme_dir = dirname( __DIR__ ); + $theme_dir = dirname( __DIR__ ); $current_theme = basename( $theme_dir ); - $theme_root = dirname( $theme_dir ); + $theme_root = dirname( $theme_dir ); - add_filter( 'theme_root', function () use ( $theme_root ) { + add_filter( 'theme_root', function() use ( $theme_root ) { return $theme_root; } ); register_theme_directory( $theme_root ); - add_filter( 'pre_option_template', function () use ( $current_theme ) { + add_filter( 'pre_option_template', function() use ( $current_theme ) { return $current_theme; - } ); - - add_filter( 'pre_option_stylesheet', function () use ( $current_theme ) { + }); + add_filter( 'pre_option_stylesheet', function() use ( $current_theme ) { return $current_theme; - } ); + }); } - tests_add_filter( 'muplugins_loaded', '_register_theme' ); + // Start up the WP testing environment. -require "{$_tests_dir}/includes/bootstrap.php"; +require $_tests_dir . '/includes/bootstrap.php'; diff --git a/utils/behat-tags.php b/utils/behat-tags.php new file mode 100644 index 000000000..f65fb7d75 --- /dev/null +++ b/utils/behat-tags.php @@ -0,0 +1,85 @@ +<?php +/** + * Generate a list of tags to skip during the test run. + * + * Require a minimum version of WordPress: + * + * @require-wp-4.0 + * Scenario: Core translation CRUD + * + * Then use in bash script: + * + * BEHAT_TAGS=$(php behat-tags.php) + * vendor/bin/behat --format progress $BEHAT_TAGS + */ + +function version_tags( $prefix, $current, $operator = '<' ) { + if ( ! $current ) + return array(); + + exec( "grep '@{$prefix}-[0-9\.]*' -h -o features/*.feature | uniq", $existing_tags ); + + $skip_tags = array(); + + foreach ( $existing_tags as $tag ) { + $compare = str_replace( "@{$prefix}-", '', $tag ); + if ( version_compare( $current, $compare, $operator ) ) { + $skip_tags[] = $tag; + } + } + + return $skip_tags; +} + +$wp_version = getenv( 'WP_VERSION' ); +$wp_version_reqs = array(); +// Only apply @require-wp tags when WP_VERSION isn't 'latest', 'nightly' or 'trunk'. +// 'latest', 'nightly' and 'trunk' are expected to work with all features. +if ( $wp_version && ! in_array( $wp_version, array( 'latest', 'nightly', 'trunk' ), true ) ) { + $wp_version_reqs = array_merge( + version_tags( 'require-wp', $wp_version, '<' ), + version_tags( 'less-than-wp', $wp_version, '>=' ) + ); +} else { + // But make sure @less-than-wp tags always exist for those special cases. (Note: @less-than-wp-latest etc won't work and shouldn't be used). + $wp_version_reqs = array_merge( $wp_version_reqs, version_tags( 'less-than-wp', '9999', '>=' ) ); +} + +$skip_tags = array_merge( + $wp_version_reqs, + version_tags( 'require-php', PHP_VERSION, '<' ), + version_tags( 'less-than-php', PHP_VERSION, '>=' ) // Note: this was '>' prior to WP-CLI 1.5.0 but the change is unlikely to cause BC issues as usually compared against major.minor only. +); + +# 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 ); +} +