From 9ed93f4bb0b33bd04fe1a3c66e04a38f991873a9 Mon Sep 17 00:00:00 2001
From: salvatorecorsi <86927009+salvatorecorsi@users.noreply.github.com>
Date: Fri, 23 Oct 2026 08:57:35 +0200
Subject: [PATCH 1/2] Tests: Preserve registered metadata between methods and
classes
---
tests/phpunit/includes/abstract-testcase.php | 78 ++++++-
.../phpunit/tests/includes/registeredMeta.php | 218 ++++++++++++++++++
tests/phpunit/tests/meta/registerMeta.php | 7 +
tests/phpunit/tests/post/meta.php | 7 +
tests/phpunit/tests/term/meta.php | 3 +
5 files changed, 312 insertions(+), 1 deletion(-)
create mode 100644 tests/phpunit/tests/includes/registeredMeta.php
diff --git a/tests/phpunit/includes/abstract-testcase.php b/tests/phpunit/includes/abstract-testcase.php
index aaf0c8aa57b5e..4b343666819db 100644
--- a/tests/phpunit/includes/abstract-testcase.php
+++ b/tests/phpunit/includes/abstract-testcase.php
@@ -32,6 +32,20 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase {
protected static $hooks_saved = array();
protected static $ignore_files;
+ /**
+ * Metadata state before each class's fixtures are created, keyed by class.
+ *
+ * @var array[]
+ */
+ private static $meta_before_class = array();
+
+ /**
+ * Metadata state before the current test, or null when setup was bypassed.
+ *
+ * @var array|null
+ */
+ private $original_meta_state;
+
/**
* The value of $GLOBALS['locale'] before each test, or null if it was unset.
*
@@ -95,7 +109,8 @@ public static function set_up_before_class() {
$wpdb->db_connect();
ini_set( 'display_errors', 1 );
- $class = get_called_class();
+ $class = get_called_class();
+ self::$meta_before_class[ $class ] = self::backup_meta_state();
if ( method_exists( $class, 'wpSetUpBeforeClass' ) ) {
call_user_func( array( $class, 'wpSetUpBeforeClass' ), static::factory() );
@@ -114,6 +129,11 @@ public static function tear_down_after_class() {
call_user_func( array( $class, 'wpTearDownAfterClass' ) );
}
+ if ( isset( self::$meta_before_class[ $class ] ) ) {
+ self::restore_meta_state( self::$meta_before_class[ $class ] );
+ unset( self::$meta_before_class[ $class ] );
+ }
+
_delete_all_data();
self::flush_cache();
@@ -142,6 +162,8 @@ public function set_up() {
$this->_backup_hooks();
}
+ $this->original_meta_state = self::backup_meta_state();
+
$this->clean_up_global_scope();
$this->original_locale = $GLOBALS['locale'] ?? null;
@@ -268,6 +290,9 @@ public function tear_down() {
remove_filter( 'query', array( $this, '_drop_temporary_tables' ) );
remove_filter( 'wp_die_handler', array( $this, 'get_wp_die_handler' ) );
$this->_restore_hooks();
+ if ( null !== $this->original_meta_state ) {
+ self::restore_meta_state( $this->original_meta_state );
+ }
wp_set_current_user( 0 );
// Restore the locale captured in set_up(); skip it for tests that bypass parent::set_up().
@@ -512,6 +537,57 @@ public static function flush_cache() {
wp_cache_add_non_persistent_groups( array( 'counts', 'plugins', 'theme_json' ) );
}
+ /**
+ * Captures registrations and their hooks without re-registering metadata.
+ *
+ * The hooks also include legacy registrations without a registry entry and
+ * callbacks added independently of register_meta().
+ *
+ * @return array Metadata registry and cloned metadata hooks.
+ */
+ private static function backup_meta_state() {
+ $state = array(
+ 'keys' => $GLOBALS['wp_meta_keys'] ?? array(),
+ 'hooks' => array(),
+ );
+
+ foreach ( $GLOBALS['wp_filter'] as $name => $hook ) {
+ if ( self::is_meta_hook( $name ) ) {
+ $state['hooks'][ $name ] = clone $hook;
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Restores metadata hooks after the ordinary hook baseline has been restored.
+ *
+ * @param array $state Metadata state captured before the test or class.
+ */
+ private static function restore_meta_state( $state ) {
+ $GLOBALS['wp_meta_keys'] = $state['keys'];
+
+ foreach ( array_keys( $GLOBALS['wp_filter'] ) as $name ) {
+ if ( self::is_meta_hook( $name ) ) {
+ unset( $GLOBALS['wp_filter'][ $name ] );
+ }
+ }
+ foreach ( $state['hooks'] as $name => $hook ) {
+ $GLOBALS['wp_filter'][ $name ] = clone $hook;
+ }
+ }
+
+ /**
+ * Identifies the dynamic hooks used by metadata registrations and defaults.
+ *
+ * @param string $name Hook name.
+ * @return bool Whether the hook belongs to the metadata state.
+ */
+ private static function is_meta_hook( $name ) {
+ return (bool) preg_match( '/^(?:sanitize|auth)_.+_meta_|^default_.+_metadata$/', $name );
+ }
+
/**
* Cleans up any registered meta keys.
*
diff --git a/tests/phpunit/tests/includes/registeredMeta.php b/tests/phpunit/tests/includes/registeredMeta.php
new file mode 100644
index 0000000000000..bb245e2f4c371
--- /dev/null
+++ b/tests/phpunit/tests/includes/registeredMeta.php
@@ -0,0 +1,218 @@
+ $object_subtype,
+ 'single' => true,
+ 'default' => 'Original default',
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'auth_callback' => '__return_true',
+ );
+ register_meta( $object_type, $key, $args );
+ $expected = get_registered_meta_keys( $object_type, $object_subtype )[ $key ];
+ $case = $this->create_test_case();
+ $class = get_class( $case );
+ $class::set_up_before_class();
+
+ try {
+ $case->set_up();
+ try {
+ if ( $unregister ) {
+ unregister_meta_key( $object_type, $key, $object_subtype );
+ } else {
+ register_meta(
+ $object_type,
+ $key,
+ array_merge(
+ $args,
+ array(
+ 'default' => 'Changed default',
+ 'auth_callback' => '__return_false',
+ )
+ )
+ );
+ }
+ register_meta( $object_type, '_test_temporary_meta', $args );
+ } finally {
+ $case->tear_down();
+ }
+
+ $next_case = new $class( 'test_second' );
+ $next_case->set_up();
+ try {
+ $registered = get_registered_meta_keys( $object_type, $object_subtype );
+ $this->assertArrayHasKey( $key, $registered );
+ $this->assertSame( $expected, $registered[ $key ] );
+ $this->assertSame( 'Example', sanitize_meta( $key, 'Example', $object_type, $object_subtype ) );
+ $hook = "auth_{$object_type}_meta_{$key}";
+ if ( $object_subtype ) {
+ $hook .= "_for_{$object_subtype}";
+ }
+ $this->assertTrue( apply_filters( $hook, false, $key, 1, 1, 'edit_post_meta', array() ) );
+ $this->assertFalse( registered_meta_key_exists( $object_type, '_test_temporary_meta', $object_subtype ) );
+ } finally {
+ $next_case->tear_down();
+ }
+ } finally {
+ $class::tear_down_after_class();
+ }
+ }
+
+ /**
+ * @return array[]
+ */
+ public static function data_registered_metadata() {
+ $cases = array();
+ foreach ( array(
+ 'post' => '',
+ 'post subtype' => 'page',
+ 'term subtype' => 'category',
+ 'user' => '',
+ 'comment' => '',
+ ) as $label => $subtype ) {
+ $type = explode( ' ', $label )[0];
+ foreach ( array( false, true ) as $unregister ) {
+ $cases[ $label . ( $unregister ? ' removed' : ' replaced' ) ] = array( $type, $subtype, $unregister );
+ }
+ }
+ return $cases;
+ }
+
+ public function test_restores_class_registrations_and_their_callbacks() {
+ // Capture the ordinary hook baseline before the class fixture exists.
+ $previous_case = $this->create_test_case();
+ $previous_case->set_up();
+ $previous_case->tear_down();
+
+ register_post_meta( 'post', '_test_plugin_meta', array( 'default' => 'Plugin default' ) );
+ $case = new class( 'test_first' ) extends WP_UnitTestCase {
+ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
+ register_post_meta(
+ 'post',
+ '_test_class_meta',
+ array(
+ 'default' => 'Class default',
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'auth_callback' => '__return_true',
+ )
+ );
+ register_post_meta( 'post', '_test_plugin_meta', array( 'default' => 'Class override' ) );
+ // Old-style registrations have callbacks but no registry entry.
+ register_meta( 'post', '_test_legacy_meta', 'sanitize_text_field' );
+ }
+
+ public function test_first() {}
+ public function test_second() {}
+ };
+ $class = get_class( $case );
+ $class::set_up_before_class();
+
+ try {
+ foreach ( array( $case, new $class( 'test_second' ) ) as $test_case ) {
+ $test_case->set_up();
+ try {
+ $registered = get_registered_meta_keys( 'post', 'post' );
+ $this->assertArrayHasKey( '_test_class_meta', $registered );
+ $this->assertArrayHasKey( '_test_plugin_meta', $registered );
+ $this->assertSame( 'Class default', $registered['_test_class_meta']['default'] );
+ $this->assertSame( 'Class override', $registered['_test_plugin_meta']['default'] );
+ $this->assertSame( 'Example', sanitize_meta( '_test_class_meta', 'Example', 'post', 'post' ) );
+ $this->assertSame( 'Example', sanitize_meta( '_test_legacy_meta', 'Example', 'post' ) );
+ unregister_post_meta( 'post', '_test_class_meta' );
+ remove_filter( 'sanitize_post_meta__test_legacy_meta', 'sanitize_text_field' );
+ } finally {
+ $test_case->tear_down();
+ }
+ }
+ } finally {
+ $class::tear_down_after_class();
+ }
+
+ $this->assertFalse( registered_meta_key_exists( 'post', '_test_class_meta', 'post' ) );
+ $registered = get_registered_meta_keys( 'post', 'post' );
+ $this->assertArrayHasKey( '_test_plugin_meta', $registered );
+ $this->assertSame( 'Plugin default', $registered['_test_plugin_meta']['default'] );
+ $this->assertSame( 'Example', sanitize_meta( '_test_legacy_meta', 'Example', 'post' ) );
+ $this->assertFalse( has_filter( 'sanitize_post_meta__test_class_meta_for_post' ) );
+ }
+
+ public function test_restores_defaults_sanitization_and_authorization_for_real_posts() {
+ register_post_meta(
+ 'post',
+ '_test_post_meta',
+ array(
+ 'single' => true,
+ 'default' => 'Original default',
+ 'sanitize_callback' => 'sanitize_text_field',
+ 'auth_callback' => '__return_true',
+ )
+ );
+ $case = new class( 'test_first' ) extends WP_UnitTestCase {
+ public static $author_id;
+ public static $post_id;
+
+ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
+ self::$author_id = $factory->user->create( array( 'role' => 'author' ) );
+ self::$post_id = $factory->post->create( array( 'post_author' => self::$author_id ) );
+ }
+
+ public function test_first() {}
+ public function test_second() {}
+ };
+ $class = get_class( $case );
+ $class::set_up_before_class();
+
+ try {
+ foreach ( array( $case, new $class( 'test_second' ) ) as $test_case ) {
+ $test_case->set_up();
+ try {
+ wp_set_current_user( $class::$author_id );
+ $this->assertTrue( current_user_can( 'edit_post_meta', $class::$post_id, '_test_post_meta' ) );
+ $this->assertSame( 'Original default', get_post_meta( $class::$post_id, '_test_post_meta', true ) );
+ update_post_meta( $class::$post_id, '_test_post_meta', 'Example' );
+ $this->assertSame( 'Example', get_post_meta( $class::$post_id, '_test_post_meta', true ) );
+ register_post_meta( 'post', '_test_post_meta', array( 'auth_callback' => '__return_false' ) );
+ $this->assertFalse( current_user_can( 'edit_post_meta', $class::$post_id, '_test_post_meta' ) );
+ } finally {
+ $test_case->tear_down();
+ }
+ }
+ } finally {
+ $class::tear_down_after_class();
+ }
+ }
+
+ /**
+ * Creates a test case with valid method names for the real setup routines.
+ *
+ * @return WP_UnitTestCase
+ */
+ private function create_test_case() {
+ return new class( 'test_first' ) extends WP_UnitTestCase {
+ public function test_first() {}
+ public function test_second() {}
+ };
+ }
+}
diff --git a/tests/phpunit/tests/meta/registerMeta.php b/tests/phpunit/tests/meta/registerMeta.php
index 16655124c95ca..1b3ad3113095b 100644
--- a/tests/phpunit/tests/meta/registerMeta.php
+++ b/tests/phpunit/tests/meta/registerMeta.php
@@ -10,6 +10,13 @@ class Tests_Meta_Register_Meta extends WP_UnitTestCase {
protected static $user_id;
protected static $blog_id;
+ public function set_up() {
+ parent::set_up();
+
+ // These tests inspect the complete registry, independently of bootstrap registrations.
+ $this->unregister_all_meta_keys();
+ }
+
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$post_id = $factory->post->create( array( 'post_type' => 'page' ) );
self::$term_id = $factory->term->create( array( 'taxonomy' => 'category' ) );
diff --git a/tests/phpunit/tests/post/meta.php b/tests/phpunit/tests/post/meta.php
index 5642f44732e83..553dce4945b2a 100644
--- a/tests/phpunit/tests/post/meta.php
+++ b/tests/phpunit/tests/post/meta.php
@@ -16,6 +16,13 @@ class Tests_Post_Meta extends WP_UnitTestCase {
protected static $post_id;
protected static $post_id_2;
+ public function set_up() {
+ parent::set_up();
+
+ // These tests inspect the complete registry, independently of bootstrap registrations.
+ $this->unregister_all_meta_keys();
+ }
+
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$author = $factory->user->create_and_get( array( 'role' => 'editor' ) );
diff --git a/tests/phpunit/tests/term/meta.php b/tests/phpunit/tests/term/meta.php
index b4149d2dd9193..4774901657b40 100644
--- a/tests/phpunit/tests/term/meta.php
+++ b/tests/phpunit/tests/term/meta.php
@@ -15,6 +15,9 @@ class Tests_Term_Meta extends WP_UnitTestCase {
public function set_up() {
parent::set_up();
+
+ // These tests inspect the complete registry, independently of bootstrap registrations.
+ $this->unregister_all_meta_keys();
register_taxonomy( 'wptests_tax', 'post' );
}
From 128bc4aea3649ff26187c220a1e2b2cacbb6b81d Mon Sep 17 00:00:00 2001
From: salvatorecorsi <86927009+salvatorecorsi@users.noreply.github.com>
Date: Sat, 31 Oct 2026 07:07:09 +0100
Subject: [PATCH 2/2] Tests: reduce isolated metadata lifecycle provider cases
---
.../phpunit/tests/includes/registeredMeta.php | 22 +++++++------------
1 file changed, 8 insertions(+), 14 deletions(-)
diff --git a/tests/phpunit/tests/includes/registeredMeta.php b/tests/phpunit/tests/includes/registeredMeta.php
index bb245e2f4c371..53e6b3d23114a 100644
--- a/tests/phpunit/tests/includes/registeredMeta.php
+++ b/tests/phpunit/tests/includes/registeredMeta.php
@@ -84,20 +84,14 @@ public function test_restores_metadata_registered_before_test_methods( $object_t
* @return array[]
*/
public static function data_registered_metadata() {
- $cases = array();
- foreach ( array(
- 'post' => '',
- 'post subtype' => 'page',
- 'term subtype' => 'category',
- 'user' => '',
- 'comment' => '',
- ) as $label => $subtype ) {
- $type = explode( ' ', $label )[0];
- foreach ( array( false, true ) as $unregister ) {
- $cases[ $label . ( $unregister ? ' removed' : ' replaced' ) ] = array( $type, $subtype, $unregister );
- }
- }
- return $cases;
+ // Each case boots WordPress separately. Cover the scopes and mutation modes
+ // without repeating their full cross-product in isolated processes.
+ return array(
+ 'post replaced' => array( 'post', '', false ),
+ 'post subtype removed' => array( 'post', 'page', true ),
+ 'term subtype replaced' => array( 'term', 'category', false ),
+ 'user removed' => array( 'user', '', true ),
+ );
}
public function test_restores_class_registrations_and_their_callbacks() {