Skip to content

Commit a68b358

Browse files
committed
Setup and run WordPress PHPUnit tests
1 parent 4b5f383 commit a68b358

8 files changed

Lines changed: 379 additions & 1 deletion

File tree

.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: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
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_RealEscape::test_real_escape_input_type_handling with data set "empty array"',
24+
'Tests_DB_RealEscape::test_real_escape_input_type_handling with data set "non-empty array"',
25+
'Tests_DB_RealEscape::test_real_escape_input_type_handling with data set "null"',
26+
'Tests_DB_RealEscape::test_real_escape_input_type_handling with data set "simple object"',
27+
'Tests_DB::test_db_reconnect',
28+
'Tests_DB::test_get_col_info',
29+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-false-1"',
30+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-false-2"',
31+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-true-1"',
32+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "escaped-true-2"',
33+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-false-1"',
34+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-false-2"',
35+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-true-1"',
36+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "format-true-2"',
37+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-false-1"',
38+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-false-2"',
39+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-true-1"',
40+
'Tests_DB::test_prepare_should_respect_the_allow_unsafe_unquoted_parameters_property with data set "numbered-true-2"',
41+
'Tests_DB::test_process_fields_value_too_long_for_field with data set "invalid chars"',
42+
'Tests_DB::test_process_fields_value_too_long_for_field with data set "too long"',
43+
'Tests_DB::test_process_fields',
44+
'Tests_DB::test_set_allowed_incompatible_sql_mode',
45+
'Tests_DB::test_set_incompatible_sql_mode',
46+
'Tests_DB::test_set_sql_mode',
47+
'Tests_Import_Import::test_double_import',
48+
'Tests_Import_Import::test_slashes_should_not_be_stripped',
49+
'Tests_Import_Import::test_small_import',
50+
'Tests_Import_Postmeta::test_serialized_postmeta_no_cdata',
51+
'Tests_Import_Postmeta::test_serialized_postmeta_with_cdata',
52+
'Tests_Import_Postmeta::test_serialized_postmeta_with_evil_stuff_in_cdata',
53+
'Tests_Import_Postmeta::test_utw_postmeta',
54+
'Tests_Meta_Query::test_convert_null_value_to_empty_string',
55+
'Tests_Meta_Query::test_null_value_sql',
56+
'Tests_Option_WpPrimeOptionCaches::test_get_option_should_return_identical_value_when_pre_primed_by_wp_prime_option_caches with data set "null"',
57+
'Tests_Option_WpPrimeOptionCaches::test_wp_prime_option_caches_cache_should_be_identical_to_get_option_cache with data set "null"',
58+
'Tests_Option_WpPrimeOptionCaches::test_wp_prime_option_caches_does_not_trigger_db_queries_for_alloptions with data set "null"',
59+
'Tests_Option_WpPrimeOptionCaches::test_wp_prime_option_caches_does_not_trigger_db_queries_repriming_options with data set "null"',
60+
'Tests_Post_Nav_Menu::test_class_applied_to_front_page_item',
61+
'Tests_Post_Nav_Menu::test_class_applied_to_privacy_policy_page_item',
62+
'Tests_Post_Nav_Menu::test_class_not_applied_to_taxonomies_with_same_id_as_front_page_item',
63+
'Tests_Post_Nav_Menu::test_iri_current_menu_item with data set #0',
64+
'Tests_Post_Nav_Menu::test_iri_current_menu_item with data set #1',
65+
'Tests_Post_Nav_Menu::test_iri_current_menu_item with data set #2',
66+
'Tests_Post_Nav_Menu::test_iri_current_menu_item with data set #3',
67+
'Tests_Post_Nav_Menu::test_iri_current_menu_item with data set #4',
68+
'Tests_Post_Nav_Menu::test_iri_current_menu_item with data set #5',
69+
'Tests_Post_Nav_Menu::test_no_front_page_class_applied',
70+
'Tests_Post_Nav_Menu::test_no_privacy_policy_class_applied',
71+
'Tests_Post_Nav_Menu::test_orphan_nav_menu_item',
72+
'Tests_Post_Nav_Menu::test_parent_ancestor_for_post_archive',
73+
'Tests_Post_Nav_Menu::test_wp_get_nav_menu_items_with_taxonomy_term',
74+
'Tests_Post_wpPost::test_get_instance_should_succeed_for_float_that_is_equal_to_post_id',
75+
'Tests_Post::test_stick_post_with_unexpected_sticky_posts_option with data set "null"',
76+
'Tests_Post::test_wp_tag_cloud_link_with_post_type',
77+
'Tests_Term_getTerms::test_wp_delete_term_should_invalidate_cache',
78+
'Tests_Term_GetTheTerms::test_term_cache_should_be_invalidated_on_remove_object_terms',
79+
'Tests_Term_GetTheTerms::test_term_cache_should_be_invalidated_on_set_object_terms',
80+
];
81+
82+
const expectedFailures = [
83+
'Tests_Comment::test_wp_new_comment_respects_comment_field_lengths',
84+
'Tests_Comment::test_wp_update_comment',
85+
'Tests_DB_dbDelta::test_column_type_change_with_hyphens_in_name',
86+
'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',
87+
'Tests_DB_dbDelta::test_query_with_backticks_does_not_throw_an_undefined_index_warning',
88+
'Tests_DB_dbDelta::test_spatial_indices',
89+
'Tests_DB::test_charset_switched_to_utf8mb4',
90+
'Tests_DB::test_close',
91+
'Tests_DB::test_delete_value_too_long_for_field with data set "too long"',
92+
'Tests_DB::test_esc_like',
93+
'Tests_DB::test_escape_and_prepare with data set #0',
94+
'Tests_DB::test_escape_and_prepare with data set #1',
95+
'Tests_DB::test_escape_and_prepare with data set #2',
96+
'Tests_DB::test_has_cap',
97+
'Tests_DB::test_insert_value_too_long_for_field with data set "too long"',
98+
'Tests_DB::test_like_query with data set #1',
99+
'Tests_DB::test_like_query with data set #3',
100+
'Tests_DB::test_like_query with data set #4',
101+
'Tests_DB::test_like_query with data set #5',
102+
'Tests_DB::test_like_query with data set #6',
103+
'Tests_DB::test_like_query with data set #8',
104+
'Tests_DB::test_mysqli_flush_sync',
105+
'Tests_DB::test_non_unicode_collations',
106+
'Tests_DB::test_query_value_contains_invalid_chars',
107+
'Tests_DB::test_replace_value_too_long_for_field with data set "too long"',
108+
'Tests_DB::test_replace',
109+
'Tests_DB::test_supports_collation',
110+
'Tests_DB::test_update_value_too_long_for_field with data set "too long"',
111+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #1',
112+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #2',
113+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #3',
114+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #4',
115+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #5',
116+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #6',
117+
'Tests_Menu_Walker_Nav_Menu::test_start_el_with_empty_attributes with data set #7',
118+
'Tests_Menu_wpNavMenu::test_parent_with_higher_id_should_not_error',
119+
'Tests_Menu_wpNavMenu::test_wp_nav_menu_should_have_has_children_class_without_custom_depth',
120+
'Tests_Menu_wpNavMenu::test_wp_nav_menu_should_not_have_has_children_class_with_custom_depth',
121+
'Tests_Post_Nav_Menu::test_wp_get_nav_menu_items_cache_primes_posts',
122+
'Tests_Post_Nav_Menu::test_wp_get_nav_menu_items_cache_primes_terms',
123+
'Tests_Post_Nav_Menu::test_wp_nav_menu_empty_container',
124+
'Tests_Post_Nav_Menu::test_wp_nav_menu_whitespace_options',
125+
'Tests_Sitemaps_Sitemaps::test_get_sitemap_entries_post_with_permalinks',
126+
'Tests_Sitemaps_Sitemaps::test_get_sitemap_entries',
127+
'Tests_Sitemaps_wpSitemapsTaxonomies::test_get_sitemap_entries_custom_taxonomies',
128+
'Tests_Sitemaps_wpSitemapsTaxonomies::test_get_url_list_custom_taxonomy',
129+
'Tests_Sitemaps_wpSitemapsTaxonomies::test_get_url_list_taxonomies',
130+
'Tests_Term_getTerms::test_get_terms_cache_should_be_missed_when_passing_number',
131+
'Tests_Term_getTerms::test_get_terms_cache',
132+
'Tests_Term_getTerms::test_get_terms_grandparent_zero',
133+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_count_hierarchical_false',
134+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_count',
135+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_idname_hierarchical_false',
136+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_idname',
137+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_idparent_hierarchical_false',
138+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_idparent',
139+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_ids_hierarchical_false',
140+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_ids',
141+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_idslug_hierarchical_false',
142+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_idslug',
143+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_names_hierarchical_false',
144+
'Tests_Term_getTerms::test_get_terms_hierarchical_tax_hide_empty_true_fields_names',
145+
'Tests_Term_getTerms::test_get_terms_parent_zero',
146+
'Tests_Term_getTerms::test_get_terms_seven_levels_deep',
147+
'Tests_Term_getTerms::test_get_terms_without_update_get_terms_cache',
148+
'Tests_Term_getTerms::test_hierarchical_false_with_child_of_and_direct_child',
149+
'Tests_Term_getTerms::test_hierarchical_should_recurse_properly_for_all_taxonomies',
150+
'Tests_Term_getTerms::test_hierarchical_true_parent_overrides_child_of',
151+
'Tests_Term_getTerms::test_hierarchical_true_with_child_of_should_return_grandchildren',
152+
'Tests_Term_getTerms::test_hierarchical_true_with_parent',
153+
'Tests_Term_getTerms::test_meta_query_args_only',
154+
'Tests_Term_GetTheTerms::test_count_should_not_be_improperly_cached',
155+
'Tests_Term::test_wp_count_terms',
156+
'WP_Test_REST_Categories_Controller::test_get_items_hide_empty_arg',
157+
'WP_Test_REST_Tags_Controller::test_get_items_hide_empty_arg',
158+
];
159+
160+
console.log( 'Running WordPress PHPUnit tests with expected failures tracking...' );
161+
console.log( 'Expected errors:', expectedErrors );
162+
console.log( 'Expected failures:', expectedFailures );
163+
164+
try {
165+
try {
166+
execSync(
167+
`composer run wp-test-phpunit -- --log-junit=phpunit-results.xml --verbose`,
168+
{ stdio: 'inherit' }
169+
);
170+
console.log( '\n⚠️ All tests passed, checking if expected errors/failures occurred...' );
171+
} catch ( error ) {
172+
console.log( '\n⚠️ Some tests errored/failed (expected). Analyzing results...' );
173+
}
174+
175+
// Read the JUnit XML test output:
176+
const junitOutputFile = path.join( __dirname, '..', '..', 'wordpress', 'phpunit-results.xml' );
177+
if ( ! fs.existsSync( junitOutputFile ) ) {
178+
console.error( 'Error: JUnit output file not found!' );
179+
process.exit( 1 );
180+
}
181+
const junitXml = fs.readFileSync( junitOutputFile, 'utf8' );
182+
183+
// Extract test info from the XML:
184+
const actualErrors = [];
185+
const actualFailures = [];
186+
for ( const testcase of junitXml.matchAll( /<testcase([^>]*)\/>|<testcase([^>]*)>([\s\S]*?)<\/testcase>/g ) ) {
187+
const attributes = {};
188+
const attributesString = testcase[2] ?? testcase[1];
189+
for ( const attribute of attributesString.matchAll( /(\w+)="([^"]*)"/g ) ) {
190+
attributes[attribute[1]] = attribute[2];
191+
}
192+
193+
const content = testcase[3] ?? '';
194+
const fqn = attributes.class ? `${attributes.class}::${attributes.name}` : attributes.name;
195+
const hasError = content.includes( '<error' );
196+
const hasFailure = content.includes( '<failure' );
197+
198+
if ( hasError ) {
199+
actualErrors.push( fqn );
200+
}
201+
202+
if ( hasFailure ) {
203+
actualFailures.push( fqn );
204+
}
205+
}
206+
207+
let isSuccess = true;
208+
209+
// Check if all expected errors actually errored
210+
const unexpectedNonErrors = expectedErrors.filter( test => ! actualErrors.includes( test ) );
211+
if ( unexpectedNonErrors.length > 0 ) {
212+
console.error( '\n❌ The following tests were expected to error but did not:' );
213+
unexpectedNonErrors.forEach( test => console.error( ` - ${test}` ) );
214+
isSuccess = false;
215+
}
216+
217+
// Check if all expected failures actually failed
218+
const unexpectedPasses = expectedFailures.filter( test => ! actualFailures.includes( test ) );
219+
if ( unexpectedPasses.length > 0 ) {
220+
console.error( '\n❌ The following tests were expected to fail but passed:' );
221+
unexpectedPasses.forEach( test => console.error( ` - ${test}` ) );
222+
isSuccess = false;
223+
}
224+
225+
// Check for unexpected errors
226+
const unexpectedErrors = actualErrors.filter( test => ! expectedErrors.includes( test ) );
227+
if ( unexpectedErrors.length > 0 ) {
228+
console.error( '\n❌ The following tests errored unexpectedly:' );
229+
unexpectedErrors.forEach( test => console.error( ` - ${test}` ) );
230+
isSuccess = false;
231+
}
232+
233+
// Check for unexpected failures
234+
const unexpectedFailures = actualFailures.filter( test => ! expectedFailures.includes( test ) );
235+
if ( unexpectedFailures.length > 0 ) {
236+
console.error( '\n❌ The following tests failed unexpectedly:' );
237+
unexpectedFailures.forEach( test => console.error( ` - ${test}` ) );
238+
isSuccess = false;
239+
}
240+
241+
if ( isSuccess ) {
242+
console.log( '\n✅ All tests behaved as expected!' );
243+
process.exit( 0 );
244+
} else {
245+
console.log( '\n❌ Some tests did not behave as expected!' );
246+
process.exit( 1 );
247+
}
248+
} catch ( error ) {
249+
console.error( '\n❌ Script execution error:', error.message );
250+
process.exit( 1 );
251+
}
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
<!--

0 commit comments

Comments
 (0)