diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..d02df98 --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,61 @@ +name: PHPUnit Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: PHP ${{ matrix.php }} - WP ${{ matrix.wp }} - ${{ matrix.multisite && 'Multisite' || 'Single Site' }} + runs-on: ubuntu-latest + + strategy: + matrix: + php: [ '7.4', '8.0' ] + wp: [ 'latest' ] + multisite: [ false, true ] + + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: wordpress_test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: Setup PHP + uses: shivammathur/setup-php@e6f75134d35752277f093989e72e140eaa222f35 # v2.30.0 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pdo, mysql, pdo_mysql, bcmath, soap, intl, gd, exif, iconv + coverage: none + + - name: Install Composer dependencies + uses: ramsey/composer-install@83af392bf5f031813d25e6fe4cd626cdba9a2df6 # v2.2.0 + + - name: Install WordPress test suite + run: | + bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 ${{ matrix.wp }} ${{ matrix.multisite && 'true' || 'false' }} + + - name: Run PHPUnit tests + run: | + if [ "${{ matrix.multisite }}" = "true" ]; then + WP_MULTISITE=1 composer test + else + composer test + fi diff --git a/.wiki/Error-Checking-Feedback-Loops.md b/.wiki/Error-Checking-Feedback-Loops.md new file mode 100644 index 0000000..53a7c70 --- /dev/null +++ b/.wiki/Error-Checking-Feedback-Loops.md @@ -0,0 +1,126 @@ +# Error Checking and Feedback Loops + +This document explains how to check for code quality issues and get feedback from automated tools in our development workflow. + +## Table of Contents + +* [Overview](#overview) +* [Local Error Checking](#local-error-checking) +* [CI/CD Feedback Loops](#cicd-feedback-loops) +* [Common Issues and Solutions](#common-issues-and-solutions) +* [Improving Code Quality](#improving-code-quality) + +## Overview + +Our development process includes multiple layers of error checking and feedback loops to ensure high code quality: + +1. **Local Development**: Run linters and tests locally before committing +2. **Pull Request**: Automated checks run when you create or update a PR +3. **Code Review**: Human reviewers provide feedback on your code +4. **Continuous Integration**: Tests run in various environments to ensure compatibility + +## Local Error Checking + +### PHP Code Quality Checks + +Run these commands locally to check for PHP code quality issues: + +```bash +# Run all PHP code quality checks +npm run lint:php + +# Run specific checks +npm run lint:phpcs # PHP CodeSniffer +npm run lint:phpstan # PHPStan static analysis +npm run lint:phpmd # PHP Mess Detector +``` + +### JavaScript/CSS Checks + +```bash +# Run ESLint for JavaScript files +npm run lint:js + +# Run stylelint for CSS files +npm run lint:css +``` + +### Running Tests Locally + +```bash +# Run Cypress tests for single site +npm run test:playground:single + +# Run Cypress tests for multisite +npm run test:playground:multisite +``` + +## CI/CD Feedback Loops + +When you push code or create a pull request, several automated checks run: + +### GitHub Actions Workflows + +* **Code Quality**: Runs PHP CodeSniffer, PHPStan, and PHP Mess Detector +* **WordPress Tests**: Runs tests in WordPress environments +* **WordPress Playground Tests**: Runs tests in WordPress Playground environments +* **Tests - Run PHP compatibility and unit tests**: Checks compatibility with different PHP versions + +### Third-Party Code Quality Services + +* **CodeFactor**: Provides automated code reviews and quality grades +* **Codacy**: Analyzes code quality and security issues +* **SonarCloud**: Detects bugs, vulnerabilities, and code smells + +## Common Issues and Solutions + +### PHP CodeSniffer Issues + +* **Indentation**: Use tabs for indentation in PHP files +* **Spacing**: Add spaces after commas, around operators, and after control structures +* **Naming Conventions**: Use snake_case for functions and variables in PHP +* **DocBlocks**: Add proper documentation for functions and classes + +### PHPStan Issues + +* **Undefined Variables**: Ensure all variables are defined before use +* **Type Errors**: Use proper type hints and return types +* **Null Checks**: Add null checks for variables that might be null + +### JavaScript/CSS Issues + +* **ESLint Errors**: Follow JavaScript best practices +* **Stylelint Errors**: Follow CSS best practices +* **Accessibility Issues**: Ensure UI elements are accessible + +## Improving Code Quality + +### Best Practices + +* **Write Tests First**: Use test-driven development (TDD) when possible +* **Small PRs**: Keep pull requests small and focused on a single issue +* **Regular Commits**: Commit frequently with clear messages +* **Code Reviews**: Request code reviews from team members +* **Documentation**: Keep documentation up-to-date + +### Using AI Assistants + +AI assistants can help you understand and fix code quality issues: + +1. Copy the error message or feedback +2. Paste it into your AI assistant chat +3. Ask for help understanding and fixing the issue +4. Apply the suggested fixes +5. Run the checks again to verify the issue is resolved + +### Continuous Learning + +* Review the code quality reports regularly +* Learn from feedback and improve your coding practices +* Stay updated on best practices for WordPress development + +## Related Documentation + +* [Testing Framework](Testing.md) +* [Code Review Guide](../.ai-workflows/code-review.md) +* [Architecture Overview](Architecture-Overview.md) diff --git a/.wiki/Testing.md b/.wiki/Testing.md index e2ec10e..ef93e20 100644 --- a/.wiki/Testing.md +++ b/.wiki/Testing.md @@ -12,6 +12,7 @@ This document explains how to use the testing framework for our plugin. * [Writing Tests](#writing-tests) * [CI/CD Integration](#cicd-integration) * [Troubleshooting](#troubleshooting) +* [PHPUnit Tests](#phpunit-tests) * [Future Improvements](#future-improvements) ## Overview @@ -200,15 +201,56 @@ We have GitHub Actions workflows for running tests in CI/CD: 1. **Docker not running**: Make sure Docker is running before starting wp-env 2. **Port conflicts**: If ports 8888 or 8889 are in use, wp-env will fail to start 3. **wp-env not installed**: Run `npm install -g @wordpress/env` to install wp-env globally +4. **WordPress Playground not loading**: Check your network connection and try refreshing the page +5. **Tests failing**: Check the error messages in the console and fix the issues + +### Error Checking and Feedback Loops + +For detailed information on how to check for code quality issues and get feedback from automated tools, see the [Error Checking and Feedback Loops](Error-Checking-Feedback-Loops.md) documentation. ### Debugging 1. **Cypress debugging**: Use `cy.debug()` to pause test execution 2. **wp-env debugging**: Run `wp-env logs` to see WordPress logs +3. **GitHub Actions debugging**: Check the workflow logs for detailed error messages + +## PHPUnit Tests + +We use PHPUnit for unit testing PHP code. The tests are located in the `tests/phpunit` directory. + +### Running PHPUnit Tests + +```bash +# Run PHPUnit tests for single site +npm run test:phpunit + +# Run PHPUnit tests for multisite +npm run test:phpunit:multisite +``` + +### Writing PHPUnit Tests + +Here's an example of a PHPUnit test for the Multisite class: + +```php +assertTrue($multisite->is_multisite_compatible()); + } + + public function test_get_network_sites() { + $multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite(); + $sites = $multisite->get_network_sites(); + $this->assertIsArray($sites); + } +} +``` ## Future Improvements -1. **PHPUnit tests**: Add unit tests for PHP code -2. **Performance tests**: Add performance testing -3. **Accessibility tests**: Add accessibility testing +1. **Performance tests**: Add performance testing +2. **Accessibility tests**: Add accessibility testing diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..1afde1b --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,159 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation] [multisite]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} +MULTISITE=${7-false} + +WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + WP_TESTS_TAG="tags/$WP_VERSION" +elif [[ $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 + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +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 + 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 "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $WP_CORE_DIR + download https://wordpress.org/nightly-builds/wordpress-latest.zip $WP_CORE_DIR/wordpress-nightly.zip + unzip -q $WP_CORE_DIR/wordpress-nightly.zip -d $WP_CORE_DIR + rm $WP_CORE_DIR/wordpress-nightly.zip + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $WP_CORE_DIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $WP_CORE_DIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $WP_CORE_DIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $WP_CORE_DIR/wordpress.tar.gz -C $WP_CORE_DIR + rm $WP_CORE_DIR/wordpress.tar.gz + fi + + 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' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + 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-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:/\+$::") + 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 + + if [ "$MULTISITE" = "true" ]; then + sed $ioption "s:// define( 'WP_TESTS_MULTISITE', true );:define( 'WP_TESTS_MULTISITE', true );:" "$WP_TESTS_DIR"/wp-tests-config.php + fi + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/composer.json b/composer.json index 1da64ad..0a6d7fc 100644 --- a/composer.json +++ b/composer.json @@ -32,12 +32,14 @@ }, "autoload": { "psr-4": { - "WPALLSTARS\\PluginStarterTemplate\\": "includes/" + "WPALLSTARS\\PluginStarterTemplate\\": "includes/", + "WP_Plugin_Starter_Template_For_AI_Coding\\": "includes/" } }, "autoload-dev": { "classmap": [ - "includes/Admin/class-admin.php" + "includes/Admin/class-admin.php", + "tests/phpunit/" ] }, "config": { diff --git a/package.json b/package.json index ea98d29..c6296ee 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "test:e2e:multisite": "npm run setup:multisite && sleep 5 && npm run test:multisite:headless", "test:playground:single": "cypress run --spec cypress/e2e/playground-single-site.cy.js", "test:playground:multisite": "cypress run --spec cypress/e2e/playground-multisite.cy.js", + "test:phpunit": "composer test", + "test:phpunit:multisite": "WP_MULTISITE=1 composer test", "build": "./build.sh", "lint:php": "composer run-script phpcs", "lint:php:simple": "composer run-script phpcs:simple", diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8a2f863 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + ./tests/phpunit/ + + + + + ./includes + + ./vendor + ./tests + ./node_modules + + + + diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php new file mode 100644 index 0000000..37a6c30 --- /dev/null +++ b/tests/phpunit/bootstrap.php @@ -0,0 +1,22 @@ +assertInstanceOf( 'WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite', $multisite ); + } + + /** + * Test is_multisite_compatible method. + */ + public function test_is_multisite_compatible() { + $multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite(); + $this->assertTrue( $multisite->is_multisite_compatible() ); + } + + /** + * Test get_network_sites method. + */ + public function test_get_network_sites() { + $multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite(); + + // Mock the get_sites function if we're not in a multisite environment. + if ( ! function_exists( 'get_sites' ) ) { + $this->assertEquals( array(), $multisite->get_network_sites() ); + } else { + $sites = $multisite->get_network_sites(); + $this->assertIsArray( $sites ); + } + } + + /** + * Test initialize_hooks method. + */ + public function test_initialize_hooks() { + $multisite = new WP_Plugin_Starter_Template_For_AI_Coding\Multisite\Multisite(); + + // Call the method. + $multisite->initialize_hooks(); + + // Check if the action was added. + $this->assertEquals( 10, has_action( 'network_admin_menu', array( $multisite, 'add_network_menu' ) ) ); + } +}