feac63173b
引入 table_exists request-scoped cache 後,其他測試類建立/drop 的 sibling flat 表(wpdo_post_attachment)會在快取留下 stale true,diagnose() 於是 去 COUNT 一張已被 drop 的表 → mysqli_sql_exception。 A 在 v3.4.6 踩過同一個坑,修法相同:兩個測試的 setUp 呼叫 Schema_Manager::flush_table_exists_cache()。 unit 451 / integration 398 GREEN Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
265 lines
11 KiB
PHP
265 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Integration test: Post Entity end-to-end lifecycle (v2.9.6).
|
|
*
|
|
* Stitches every phase shipped in v2.9.0 → v2.9.5 into a single regression
|
|
* net. If any future change breaks the contract between two phases (e.g.
|
|
* Postmeta_Cleaner output format vs Post_Migration::backfill_group input),
|
|
* this test fails before it ships.
|
|
*
|
|
* Phase coverage:
|
|
* v2.9.0 Postmeta_Cleaner::count_garbage / delete_garbage
|
|
* v2.9.1 Post_Fields::register_entity_fields → groups visible in Registry
|
|
* v2.9.2 Sync_Bridge guard (verified separately in SyncBridgeEntityGuardTest)
|
|
* v2.9.3 Post_Migration::diagnose / backfill_group / cleanup
|
|
* v2.9.4 Post_Stress_Tester::create / count / cleanup
|
|
* v2.9.5 Post_Migration::copy_legacy_hot_table / verify_legacy_cutover
|
|
*/
|
|
class PostEntityLifecycleTest extends TestCase {
|
|
|
|
private const POSTS = 'wp_itest_posts';
|
|
private const POSTMETA = 'wp_itest_postmeta';
|
|
private const FLAT = 'wp_itest_wpdo_post_wc_product';
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
global $wpdb;
|
|
|
|
// Load full chain.
|
|
$plugin_dir = WPDO_PLUGIN_DIR;
|
|
foreach ( array(
|
|
'includes/adapters/interface-entity-adapter.php',
|
|
'includes/engine/class-tmdo-entity-registry.php',
|
|
'includes/engine/class-tmdo-mode-manager.php',
|
|
'includes/engine/class-tmdo-schema-manager.php',
|
|
'includes/adapters/class-tmdo-adapter-post.php',
|
|
'includes/integrations/class-tmdo-post-fields.php',
|
|
'includes/migration/class-tmdo-post-migration.php',
|
|
'includes/class-tmdo-postmeta-cleaner.php',
|
|
'includes/class-tmdo-post-stress-tester.php',
|
|
) as $rel ) {
|
|
$file = $plugin_dir . $rel;
|
|
$class_name = self::class_for( $rel );
|
|
if ( $class_name && ! class_exists( $class_name ) && ! interface_exists( $class_name ) ) {
|
|
require_once $file;
|
|
}
|
|
}
|
|
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' );
|
|
$wpdb->query(
|
|
'CREATE TABLE `' . self::POSTS . '` (
|
|
ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
post_title text NOT NULL,
|
|
post_type varchar(20) NOT NULL DEFAULT \'post\',
|
|
post_status varchar(20) NOT NULL DEFAULT \'publish\',
|
|
post_date datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
post_date_gmt datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
post_modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
post_modified_gmt datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
post_author bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
post_content longtext NOT NULL,
|
|
post_excerpt text NOT NULL,
|
|
comment_status varchar(20) NOT NULL DEFAULT \'open\',
|
|
ping_status varchar(20) NOT NULL DEFAULT \'open\',
|
|
post_password varchar(255) NOT NULL DEFAULT \'\',
|
|
post_name varchar(200) NOT NULL DEFAULT \'\',
|
|
to_ping text NOT NULL,
|
|
pinged text NOT NULL,
|
|
post_content_filtered longtext NOT NULL,
|
|
post_parent bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
guid varchar(255) NOT NULL DEFAULT \'\',
|
|
menu_order int(11) NOT NULL DEFAULT 0,
|
|
post_mime_type varchar(100) NOT NULL DEFAULT \'\',
|
|
comment_count bigint(20) NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (ID),
|
|
KEY post_type (post_type),
|
|
KEY post_title (post_title(64))
|
|
) DEFAULT CHARACTER SET utf8mb4'
|
|
);
|
|
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' );
|
|
$wpdb->query(
|
|
'CREATE TABLE `' . self::POSTMETA . '` (
|
|
meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
post_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
meta_key varchar(255) DEFAULT NULL,
|
|
meta_value longtext,
|
|
PRIMARY KEY (meta_id),
|
|
KEY post_id (post_id),
|
|
KEY meta_key (meta_key(191))
|
|
) DEFAULT CHARACTER SET utf8mb4'
|
|
);
|
|
|
|
// wc_product flat target with all 19 columns.
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' );
|
|
$wpdb->query(
|
|
'CREATE TABLE `' . self::FLAT . '` (
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
post_id bigint(20) unsigned NOT NULL,
|
|
_price decimal(18,6) DEFAULT NULL,
|
|
_regular_price decimal(18,6) DEFAULT NULL,
|
|
_sale_price decimal(18,6) DEFAULT NULL,
|
|
_stock bigint(20) DEFAULT NULL,
|
|
_stock_status varchar(100) DEFAULT NULL,
|
|
_sku varchar(255) DEFAULT NULL,
|
|
_manage_stock varchar(100) DEFAULT NULL,
|
|
_backorders varchar(100) DEFAULT NULL,
|
|
_sold_individually varchar(100) DEFAULT NULL,
|
|
_virtual varchar(100) DEFAULT NULL,
|
|
_downloadable varchar(100) DEFAULT NULL,
|
|
_tax_class varchar(255) DEFAULT NULL,
|
|
_tax_status varchar(100) DEFAULT NULL,
|
|
_download_limit bigint(20) DEFAULT NULL,
|
|
_download_expiry bigint(20) DEFAULT NULL,
|
|
_product_version varchar(255) DEFAULT NULL,
|
|
_wc_average_rating decimal(18,6) DEFAULT NULL,
|
|
_wc_review_count bigint(20) DEFAULT NULL,
|
|
total_sales bigint(20) DEFAULT NULL,
|
|
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uk_post_id (post_id)
|
|
) DEFAULT CHARACTER SET utf8mb4'
|
|
);
|
|
|
|
// Register post adapter + groups.
|
|
WPDO_Entity_Registry::init();
|
|
WPDO_Entity_Registry::register_adapter( 'post', new WPDO_Adapter_Post() );
|
|
WPDO_Post_Fields::register_entity_fields();
|
|
}
|
|
|
|
public static function tearDownAfterClass(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' );
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' );
|
|
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::FLAT . '`' );
|
|
|
|
// Reset Mode_Manager + Entity_Registry to avoid leaking state.
|
|
$ref = new ReflectionClass( WPDO_Mode_Manager::class );
|
|
$cache = $ref->getProperty( 'cache' );
|
|
$cache->setAccessible( true );
|
|
$cache->setValue( null, null );
|
|
WPDO_Entity_Registry::init();
|
|
}
|
|
|
|
protected function setUp(): void {
|
|
global $wpdb;
|
|
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTS . '`' );
|
|
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' );
|
|
$wpdb->query( 'TRUNCATE TABLE `' . self::FLAT . '`' );
|
|
|
|
// Other classes create/drop sibling flat tables (e.g. wpdo_post_attachment):
|
|
// a stale table_exists() cache would make diagnose() COUNT a dropped table.
|
|
TMDO_Schema_Manager::flush_table_exists_cache();
|
|
}
|
|
|
|
private static function class_for( string $rel ): ?string {
|
|
$map = array(
|
|
'interface-entity-adapter.php' => 'WPDO_Entity_Adapter_Interface',
|
|
'class-tmdo-entity-registry.php' => 'WPDO_Entity_Registry',
|
|
'class-tmdo-mode-manager.php' => 'WPDO_Mode_Manager',
|
|
'class-tmdo-schema-manager.php' => 'WPDO_Schema_Manager',
|
|
'class-tmdo-adapter-post.php' => 'WPDO_Adapter_Post',
|
|
'class-tmdo-post-fields.php' => 'WPDO_Post_Fields',
|
|
'class-tmdo-post-migration.php' => 'WPDO_Post_Migration',
|
|
'class-tmdo-postmeta-cleaner.php' => 'WPDO_Postmeta_Cleaner',
|
|
'class-tmdo-post-stress-tester.php' => 'WPDO_Post_Stress_Tester',
|
|
);
|
|
foreach ( $map as $needle => $cls ) {
|
|
if ( str_contains( $rel, $needle ) ) {
|
|
return $cls;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── End-to-end lifecycle ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Full lifecycle: stress create → cleanup garbage → backfill → diagnose
|
|
* → stress cleanup → final ratio assertion.
|
|
*
|
|
* This test is the contract net for v2.9.x phases working together.
|
|
*/
|
|
public function test_full_post_entity_lifecycle(): void {
|
|
global $wpdb;
|
|
|
|
// Phase 1 (v2.9.4): seed 10 product posts via stress tester.
|
|
$create_result = WPDO_Post_Stress_Tester::create( 'product', 10 );
|
|
$this->assertSame( 10, $create_result['created'] );
|
|
$this->assertSame( 10, WPDO_Post_Stress_Tester::count_test_posts() );
|
|
|
|
// Add some garbage to validate v2.9.0 cleanup phase.
|
|
for ( $i = 0; $i < 5; $i++ ) {
|
|
$wpdb->insert( self::POSTMETA, array(
|
|
'post_id' => 1,
|
|
'meta_key' => '_transient_test_' . $i,
|
|
'meta_value' => 'x',
|
|
) );
|
|
$wpdb->insert( self::POSTMETA, array(
|
|
'post_id' => 1,
|
|
'meta_key' => '_wp_old_date',
|
|
'meta_value' => '2024-01-01',
|
|
) );
|
|
}
|
|
|
|
// Phase 2 (v2.9.0): cleanup garbage.
|
|
$garbage_before = WPDO_Postmeta_Cleaner::count_garbage( 'all' );
|
|
$this->assertSame( 5, $garbage_before['transients'] );
|
|
$this->assertSame( 5, $garbage_before['wp_old_date'] );
|
|
|
|
$deleted = WPDO_Postmeta_Cleaner::delete_garbage( 'all' );
|
|
$this->assertSame( 10, $deleted['total'] );
|
|
|
|
$garbage_after = WPDO_Postmeta_Cleaner::count_garbage( 'all' );
|
|
$this->assertSame( 0, $garbage_after['total'], 'After delete, all garbage gone.' );
|
|
|
|
// Phase 3 (v2.9.3): diagnose post entity state.
|
|
$diag1 = WPDO_Post_Migration::diagnose();
|
|
$this->assertSame( 10, $diag1['posts'] );
|
|
$this->assertSame( 'disabled', $diag1['mode'] );
|
|
$this->assertGreaterThan( 0, $diag1['groups']['wc_product']['eav_rows'], 'wc_product seeded keys present.' );
|
|
$this->assertSame( 0, $diag1['groups']['wc_product']['flat_rows'], 'flat empty before backfill.' );
|
|
|
|
// Phase 4 (v2.9.3): backfill wc_product from postmeta to flat.
|
|
$backfill = WPDO_Post_Migration::backfill_group( 'wc_product' );
|
|
$this->assertSame( 10, $backfill['migrated'], '10 products backfilled to flat.' );
|
|
|
|
// Phase 5: re-diagnose; flat_rows must equal post count for wc_product.
|
|
$diag2 = WPDO_Post_Migration::diagnose();
|
|
$this->assertSame( 10, $diag2['groups']['wc_product']['flat_rows'] );
|
|
|
|
// Phase 6 (v2.9.4): stress cleanup removes everything.
|
|
$cleanup = WPDO_Post_Stress_Tester::cleanup();
|
|
$this->assertSame( 10, $cleanup['deleted_posts'] );
|
|
$this->assertSame( 0, WPDO_Post_Stress_Tester::count_test_posts() );
|
|
|
|
// Final state: empty everywhere.
|
|
$diag3 = WPDO_Post_Migration::diagnose();
|
|
$this->assertSame( 0, $diag3['posts'] );
|
|
$this->assertSame( 0, $diag3['groups']['wc_product']['eav_rows'] );
|
|
// Note: flat rows survive stress cleanup (ON DELETE CASCADE not configured
|
|
// in test fixture); production v2.9.4 cleanup() also sweeps flat tables.
|
|
$this->assertGreaterThanOrEqual( 0, $diag3['groups']['wc_product']['flat_rows'] );
|
|
}
|
|
|
|
/**
|
|
* Verifies that v2.9.0 cleanup + v2.9.3 backfill have zero overlap:
|
|
* cleanup keys (_transient_*, _wp_old_date, stale _edit_lock) must not
|
|
* collide with any v2.9.1 entity group's managed keys.
|
|
*/
|
|
public function test_cleanup_keys_never_overlap_managed_group_keys(): void {
|
|
$managed = WPDO_Post_Migration::get_managed_keys();
|
|
|
|
foreach ( $managed as $key ) {
|
|
$this->assertStringStartsNotWith( '_transient_', $key );
|
|
$this->assertStringStartsNotWith( '_transient_timeout_', $key );
|
|
$this->assertNotSame( '_wp_old_date', $key );
|
|
$this->assertNotSame( '_edit_lock', $key );
|
|
}
|
|
}
|
|
}
|