Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.

Commit 545f95a

Browse files
authored
Merge pull request #26 from Automattic/wp-unit-tests
WordPress unit tests
2 parents fac0506 + 2d0e59d commit 545f95a

File tree

10 files changed

+320
-6
lines changed

10 files changed

+320
-6
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
composer.json export-ignore
66
phpcs.xml.dist export-ignore
77
phpunit.xml.dist export-ignore
8+
wp-setup.sh export-ignore
89
/.github export-ignore
910
/grammar-tools export-ignore
1011
/tests export-ignore
1112
/wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php export-ignore
13+
/wordpress export-ignore
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Wrap the "composer run wp-tests-phpunit" command to process tests
3+
* that are expected to error and fail at the moment.
4+
*
5+
* This makes sure that the CI job passes, while explicitly tracking
6+
* the issues that need to be addressed. Ideally, over time this script
7+
* will become obsolete when all errors and failures are resolved.
8+
*/
9+
const { execSync } = require( 'child_process' );
10+
const fs = require( 'fs' );
11+
const path = require( 'path' );
12+
13+
const expectedErrors = [
14+
'Tests_Admin_wpSiteHealth::test_object_cache_default_thresholds_non_multisite',
15+
'Tests_Admin_wpSiteHealth::test_object_cache_thresholds with data set #0',
16+
'Tests_Admin_wpSiteHealth::test_object_cache_thresholds with data set #1',
17+
'Tests_Admin_wpSiteHealth::test_object_cache_thresholds with data set #2',
18+
'Tests_Admin_wpSiteHealth::test_object_cache_thresholds with data set #3',
19+
'Tests_Admin_wpSiteHealth::test_object_cache_thresholds with data set #4',
20+
'Tests_Comment_WpComment::test_get_instance_should_succeed_for_float_that_is_equal_to_post_id',
21+
'Tests_Cron_getCronArray::test_get_cron_array_output_validation with data set "null"',
22+
'Tests_DB_Charset::test_strip_invalid_text',
23+
'Tests_DB::test_db_reconnect',
24+
'Tests_DB::test_get_col_info',
25+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-false-1"',
26+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-false-2"',
27+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-true-1"',
28+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-true-2"',
29+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-false-1"',
30+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-false-2"',
31+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-true-1"',
32+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-true-2"',
33+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-false-1"',
34+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-false-2"',
35+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-true-1"',
36+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-true-2"',
37+
'Tests_DB::test_process_fields_value_too_long_for_field with data set "invalid chars"',
38+
'Tests_DB::test_process_fields_value_too_long_for_field with data set "too long"',
39+
'Tests_DB::test_process_fields',
40+
'Tests_DB::test_set_allowed_incompatible_sql_mode',
41+
'Tests_DB::test_set_incompatible_sql_mode',
42+
'Tests_DB::test_set_sql_mode',
43+
'Tests_Post_wpPost::test_get_instance_should_succeed_for_float_that_is_equal_to_post_id',
44+
'Tests_Post::test_stick_post_with_unexpected_sticky_posts_option with data set "null"',
45+
];
46+
47+
const expectedFailures = [
48+
'Tests_Comment::test_wp_new_comment_respects_comment_field_lengths',
49+
'Tests_Comment::test_wp_update_comment',
50+
'Tests_DB_dbDelta::test_column_type_change_with_hyphens_in_name',
51+
'Tests_DB_dbDelta::test_query_with_backticks_does_not_cause_a_query_to_alter_all_columns_and_indices_to_run_even_if_none_have_changed',
52+
'Tests_DB_dbDelta::test_query_with_backticks_does_not_throw_an_undefined_index_warning',
53+
'Tests_DB_dbDelta::test_spatial_indices',
54+
'Tests_DB::test_charset_switched_to_utf8mb4',
55+
'Tests_DB::test_close',
56+
'Tests_DB::test_delete_value_too_long_for_field with data set "too long"',
57+
'Tests_DB::test_has_cap',
58+
'Tests_DB::test_insert_value_too_long_for_field with data set "too long"',
59+
'Tests_DB::test_mysqli_flush_sync',
60+
'Tests_DB::test_non_unicode_collations',
61+
'Tests_DB::test_query_value_contains_invalid_chars',
62+
'Tests_DB::test_replace_value_too_long_for_field with data set "too long"',
63+
'Tests_DB::test_replace',
64+
'Tests_DB::test_supports_collation',
65+
'Tests_DB::test_update_value_too_long_for_field with data set "too long"',
66+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #1',
67+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #2',
68+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #3',
69+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #4',
70+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #5',
71+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #6',
72+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #7',
73+
'Tests_Menu_wpNavMenu::test_wp_nav_menu_should_not_have_has_children_class_with_custom_depth',
74+
];
75+
76+
console.log( 'Running WordPress PHPUnit tests with expected failures tracking...' );
77+
console.log( 'Expected errors:', expectedErrors );
78+
console.log( 'Expected failures:', expectedFailures );
79+
80+
try {
81+
try {
82+
execSync(
83+
`composer run wp-test-phpunit -- --log-junit=phpunit-results.xml --verbose`,
84+
{ stdio: 'inherit' }
85+
);
86+
console.log( '\n⚠️ All tests passed, checking if expected errors/failures occurred...' );
87+
} catch ( error ) {
88+
console.log( '\n⚠️ Some tests errored/failed (expected). Analyzing results...' );
89+
}
90+
91+
// Read the JUnit XML test output:
92+
const junitOutputFile = path.join( __dirname, '..', '..', 'wordpress', 'phpunit-results.xml' );
93+
if ( ! fs.existsSync( junitOutputFile ) ) {
94+
console.error( 'Error: JUnit output file not found!' );
95+
process.exit( 1 );
96+
}
97+
const junitXml = fs.readFileSync( junitOutputFile, 'utf8' );
98+
99+
// Extract test info from the XML:
100+
const actualErrors = [];
101+
const actualFailures = [];
102+
for ( const testcase of junitXml.matchAll( /<testcase([^>]*)\/>|<testcase([^>]*)>([\s\S]*?)<\/testcase>/g ) ) {
103+
const attributes = {};
104+
const attributesString = testcase[2] ?? testcase[1];
105+
for ( const attribute of attributesString.matchAll( /(\w+)="([^"]*)"/g ) ) {
106+
attributes[attribute[1]] = attribute[2];
107+
}
108+
109+
const content = testcase[3] ?? '';
110+
const fqn = attributes.class ? `${attributes.class}::${attributes.name}` : attributes.name;
111+
const hasError = content.includes( '<error' );
112+
const hasFailure = content.includes( '<failure' );
113+
114+
if ( hasError ) {
115+
actualErrors.push( fqn );
116+
}
117+
118+
if ( hasFailure ) {
119+
actualFailures.push( fqn );
120+
}
121+
}
122+
123+
let isSuccess = true;
124+
125+
// Check if all expected errors actually errored
126+
const unexpectedNonErrors = expectedErrors.filter( test => ! actualErrors.includes( test ) );
127+
if ( unexpectedNonErrors.length > 0 ) {
128+
console.error( '\n❌ The following tests were expected to error but did not:' );
129+
unexpectedNonErrors.forEach( test => console.error( ` - ${test}` ) );
130+
isSuccess = false;
131+
}
132+
133+
// Check if all expected failures actually failed
134+
const unexpectedPasses = expectedFailures.filter( test => ! actualFailures.includes( test ) );
135+
if ( unexpectedPasses.length > 0 ) {
136+
console.error( '\n❌ The following tests were expected to fail but passed:' );
137+
unexpectedPasses.forEach( test => console.error( ` - ${test}` ) );
138+
isSuccess = false;
139+
}
140+
141+
// Check for unexpected errors
142+
const unexpectedErrors = actualErrors.filter( test => ! expectedErrors.includes( test ) );
143+
if ( unexpectedErrors.length > 0 ) {
144+
console.error( '\n❌ The following tests errored unexpectedly:' );
145+
unexpectedErrors.forEach( test => console.error( ` - ${test}` ) );
146+
isSuccess = false;
147+
}
148+
149+
// Check for unexpected failures
150+
const unexpectedFailures = actualFailures.filter( test => ! expectedFailures.includes( test ) );
151+
if ( unexpectedFailures.length > 0 ) {
152+
console.error( '\n❌ The following tests failed unexpectedly:' );
153+
unexpectedFailures.forEach( test => console.error( ` - ${test}` ) );
154+
isSuccess = false;
155+
}
156+
157+
if ( isSuccess ) {
158+
console.log( '\n✅ All tests behaved as expected!' );
159+
process.exit( 0 );
160+
} else {
161+
console.log( '\n❌ Some tests did not behave as expected!' );
162+
process.exit( 1 );
163+
}
164+
} catch ( error ) {
165+
console.error( '\n❌ Script execution error:', error.message );
166+
process.exit( 1 );
167+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: WordPress Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
test:
11+
name: WordPress PHPUnit Tests
12+
runs-on: ubuntu-latest
13+
timeout-minutes: 20
14+
15+
steps:
16+
- name: Checkout repository
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Docker Buildx
20+
uses: docker/setup-buildx-action@v3
21+
22+
- name: Set UID and GID for PHP in WordPress images
23+
run: |
24+
echo "PHP_FPM_UID=$(id -u)" >> $GITHUB_ENV
25+
echo "PHP_FPM_GID=$(id -g)" >> $GITHUB_ENV
26+
27+
- name: Setup WordPress test environment
28+
run: composer run wp-setup
29+
30+
- name: Start WordPress test environment
31+
run: composer run wp-test-start
32+
33+
- name: Run WordPress PHPUnit tests
34+
run: node .github/workflows/wp-tests-phpunit-run.js
35+
36+
- name: Stop Docker containers
37+
if: always()
38+
run: composer run wp-test-clean

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ composer.lock
55
._.DS_Store
66
.DS_Store
77
._*
8+
/wordpress

composer.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"allow-plugins": {
2727
"dealerdirect/phpcodesniffer-composer-installer": true,
2828
"phpstan/extension-installer": true
29-
}
29+
},
30+
"process-timeout": 3600
3031
},
3132
"scripts": {
3233
"check-cs": [
@@ -37,6 +38,22 @@
3738
],
3839
"test": [
3940
"phpunit"
41+
],
42+
"wp-setup": [
43+
"./wp-setup.sh"
44+
],
45+
"wp-run": [
46+
"npm --prefix wordpress run"
47+
],
48+
"wp-test-start": [
49+
"npm --prefix wordpress run env:start",
50+
"npm --prefix wordpress run env:install"
51+
],
52+
"wp-test-phpunit": [
53+
"npm --prefix wordpress run test:php --"
54+
],
55+
"wp-test-clean": [
56+
"npm --prefix wordpress run env:clean"
4057
]
4158
}
4259
}

constants.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,8 @@
5151
define( 'FQDB', FQDBDIR . '.ht.sqlite' );
5252
}
5353
}
54+
55+
// Allow enabling the SQLite AST driver via environment variable.
56+
if ( ! defined( 'WP_SQLITE_AST_DRIVER' ) && 'true' === $_ENV['WP_SQLITE_AST_DRIVER'] ) {
57+
define( 'WP_SQLITE_AST_DRIVER', true );
58+
}

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
<!-- Directories and third party library exclusions. -->
3333
<exclude-pattern>/vendor/*</exclude-pattern>
34+
<exclude-pattern>/wordpress/*</exclude-pattern>
3435
<exclude-pattern>/wp-includes/sqlite/class-wp-sqlite-crosscheck-db.php</exclude-pattern>
3536

3637
<!--

wp-includes/sqlite/class-wp-sqlite-db.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,16 @@ public function select( $db, $dbh = null ) {
9999
*
100100
* @see wpdb::_real_escape()
101101
*
102-
* @param string $str The string to escape.
102+
* @param string $data The string to escape.
103103
*
104104
* @return string escaped
105105
*/
106-
public function _real_escape( $str ) {
107-
return addslashes( $str );
106+
public function _real_escape( $data ) {
107+
if ( ! is_scalar( $data ) ) {
108+
return '';
109+
}
110+
$escaped = addslashes( $data );
111+
return $this->add_placeholder_escape( $escaped );
108112
}
109113

110114
/**
@@ -121,6 +125,11 @@ public function _real_escape( $str ) {
121125
* or real_escape next.
122126
*/
123127
public function esc_like( $text ) {
128+
// The new driver adds "ESCAPE '\\'" to every LIKE expression by default.
129+
// We only need to overload this function to a no-op for the old driver.
130+
if ( $this->dbh instanceof WP_SQLite_Driver ) {
131+
return parent::esc_like( $text );
132+
}
124133
return $text;
125134
}
126135

wp-includes/sqlite/install-functions.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
* @throws PDOException If the database connection fails.
1919
*/
2020
function sqlite_make_db_sqlite() {
21+
global $wpdb;
22+
2123
include_once ABSPATH . 'wp-admin/includes/schema.php';
2224

2325
$table_schemas = wp_get_db_schema();
@@ -31,8 +33,17 @@ function sqlite_make_db_sqlite() {
3133
wp_die( $message, 'Database Error!' );
3234
}
3335

34-
$translator = new WP_SQLite_Translator( $pdo );
35-
$query = null;
36+
if ( defined( 'WP_SQLITE_AST_DRIVER' ) && WP_SQLITE_AST_DRIVER ) {
37+
$translator = new WP_SQLite_Driver(
38+
array(
39+
'database' => $wpdb->dbname,
40+
'connection' => $pdo,
41+
)
42+
);
43+
} else {
44+
$translator = new WP_SQLite_Translator( $pdo );
45+
}
46+
$query = null;
3647

3748
try {
3849
$translator->begin_transaction();

wp-setup.sh

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/bin/bash
2+
3+
##
4+
# This script prepares the WordPress repository for tests and development.
5+
# It clones the WordPress repository and makes sure that the SQLite plugin
6+
# is used in the development and testing environment instead of MySQL.
7+
##
8+
9+
set -e
10+
11+
WP_VERSION="6.7.2"
12+
13+
DIR="$(dirname "$0")"
14+
WP_DIR="$DIR/wordpress"
15+
16+
# 1. Ensure that Git is installed.
17+
echo "Checking if Git is installed..."
18+
if ! command -v git &> /dev/null; then
19+
echo 'Error: Git is not installed.' >&2
20+
exit 1
21+
fi
22+
23+
# 2. Clone the WordPress repository, if it doesn't exist.
24+
echo "Cleaning up the WordPress repository..."
25+
rm -rf "$WP_DIR"
26+
echo "Cloning the WordPress repository..."
27+
git clone --depth 1 --branch "$WP_VERSION" https://github.com/WordPress/wordpress-develop.git "$WP_DIR"
28+
29+
# 3. Add "docker-compose.override.yml" to the WordPress repository.
30+
echo "Adding 'docker-compose.override.yml' to the WordPress repository..."
31+
cat << EOF > "$WP_DIR/docker-compose.override.yml"
32+
services:
33+
wordpress-develop:
34+
environment:
35+
WP_SQLITE_AST_DRIVER: true
36+
volumes:
37+
- ../:/var/www/src/wp-content/plugins/sqlite-database-integration
38+
39+
php:
40+
image: wordpressdevelop/php:8.3-fpm
41+
environment:
42+
WP_SQLITE_AST_DRIVER: true
43+
volumes:
44+
- ../:/var/www/src/wp-content/plugins/sqlite-database-integration
45+
46+
cli:
47+
image: wordpressdevelop/cli:8.3-fpm
48+
environment:
49+
WP_SQLITE_AST_DRIVER: true
50+
volumes:
51+
- ../:/var/www/src/wp-content/plugins/sqlite-database-integration
52+
EOF
53+
54+
# 4. Add "db.php" to the "wp-content" directory.
55+
echo "Adding 'db.php' to the 'wp-content' directory..."
56+
rm -f "$WP_DIR"/src/wp-content/db.php
57+
cp "$DIR"/db.copy "$WP_DIR"/src/wp-content/db.php
58+
sed -i.bak "s#'{SQLITE_IMPLEMENTATION_FOLDER_PATH}'#__DIR__.'/plugins/sqlite-database-integration'#g" "$WP_DIR"/src/wp-content/db.php
59+
sed -i.bak "s#{SQLITE_PLUGIN}#$WP_DIR/src/wp-content/plugins/sqlite-database-integration/load.php#g" "$WP_DIR"/src/wp-content/db.php
60+
61+
# 5. Install dependencies.
62+
echo "Installing dependencies..."
63+
npm --prefix "$WP_DIR" install

0 commit comments

Comments
 (0)