Files
2meet-data-optimizer/tests/integration/PostMigrationTest.php
T
wpdev feac63173b test(migration): PostMigrationTest / PostEntityLifecycleTest 補 flush_table_exists_cache
引入 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
2026-07-31 05:56:36 +08:00

245 lines
9.3 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Integration test: WPDO_Post_Migration core methods (v2.9.3).
*
* Verifies diagnose(), backfill_group(), cutover() against real MariaDB,
* mirroring the user-side WPDO_Migration_Orchestrator's contract but using
* a new independent class so the user orchestrator's 1105 lines stay frozen.
*
* Requires real MariaDB (WPDO_TEST_DB_PASS env var must be set).
*/
class PostMigrationTest extends TestCase {
private const POSTS = 'wp_itest_posts';
private const POSTMETA = 'wp_itest_postmeta';
private const FLAT = 'wp_itest_wpdo_post_wc_product';
// ── Fixture lifecycle ─────────────────────────────────────────────────────
public static function setUpBeforeClass(): void {
global $wpdb;
// Load post entity chain.
if ( ! interface_exists( 'WPDO_Entity_Adapter_Interface' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/adapters/interface-entity-adapter.php';
}
if ( ! class_exists( 'WPDO_Entity_Registry' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/engine/class-tmdo-entity-registry.php';
}
if ( ! class_exists( 'WPDO_Mode_Manager' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/engine/class-tmdo-mode-manager.php';
}
if ( ! class_exists( 'WPDO_Schema_Manager' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/engine/class-tmdo-schema-manager.php';
}
if ( ! class_exists( 'WPDO_Adapter_Post' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/adapters/class-tmdo-adapter-post.php';
}
if ( ! class_exists( 'WPDO_Post_Fields' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/integrations/class-tmdo-post-fields.php';
}
if ( ! class_exists( 'WPDO_Post_Migration' ) ) {
require_once WPDO_PLUGIN_DIR . 'includes/migration/class-tmdo-post-migration.php';
}
// wp_posts (minimal — only ID + post_type).
$wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' );
$wpdb->query(
'CREATE TABLE `' . self::POSTS . '` (
ID bigint(20) unsigned NOT NULL AUTO_INCREMENT,
post_type varchar(20) NOT NULL DEFAULT \'post\',
PRIMARY KEY (ID),
KEY post_type (post_type)
) DEFAULT CHARACTER SET utf8mb4'
);
// wp_postmeta — same schema as production WP.
$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'
);
// Flat target for wc_product group (subset of full schema for test).
$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,
_stock bigint(20) DEFAULT NULL,
_stock_status varchar(100) DEFAULT NULL,
_sku varchar(255) 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'
);
// Reset Entity Registry + register post adapter + post fields.
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 polluting later tests.
$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 function seed_post( int $id, string $post_type ): void {
global $wpdb;
$wpdb->insert( self::POSTS, array( 'ID' => $id, 'post_type' => $post_type ) );
}
private function seed_meta( int $post_id, string $key, string $value ): void {
global $wpdb;
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => $key, 'meta_value' => $value ) );
}
// ── diagnose() ────────────────────────────────────────────────────────────
public function test_diagnose_reports_posts_postmeta_ratio(): void {
// 5 posts, 12 postmeta rows → ratio 2.4
for ( $i = 1; $i <= 5; $i++ ) {
$this->seed_post( $i, 'post' );
}
for ( $i = 0; $i < 12; $i++ ) {
$this->seed_meta( ( $i % 5 ) + 1, 'random_key', 'v' );
}
$result = WPDO_Post_Migration::diagnose();
$this->assertSame( 5, $result['posts'] );
$this->assertSame( 12, $result['postmeta'] );
$this->assertSame( 2.4, $result['ratio'] );
}
public function test_diagnose_reports_eav_rows_per_managed_group(): void {
$this->seed_post( 1, 'product' );
$this->seed_meta( 1, '_price', '99.99' );
$this->seed_meta( 1, '_stock', '5' );
$this->seed_meta( 1, 'unrelated_key', 'x' ); // not in any group
$result = WPDO_Post_Migration::diagnose();
$this->assertArrayHasKey( 'groups', $result );
$this->assertArrayHasKey( 'wc_product', $result['groups'] );
$this->assertSame(
2,
$result['groups']['wc_product']['eav_rows'],
'wc_product group has 2 EAV rows: _price + _stock (unrelated_key excluded).'
);
}
public function test_diagnose_reports_zero_eav_for_unused_group(): void {
$this->seed_post( 1, 'post' );
$this->seed_meta( 1, 'random_key', 'v' );
$result = WPDO_Post_Migration::diagnose();
// nav_menu_item group has no postmeta seeded.
$this->assertSame( 0, $result['groups']['nav_menu_item']['eav_rows'] );
}
public function test_diagnose_reports_post_mode(): void {
$result = WPDO_Post_Migration::diagnose();
$this->assertArrayHasKey( 'mode', $result );
// Default post mode is 'disabled' per Mode_Manager defaults().
$this->assertSame( 'disabled', $result['mode'] );
}
// ── backfill_group() — bulk SQL pivot ─────────────────────────────────────
public function test_backfill_group_pivots_wc_product_keys(): void {
$this->seed_post( 1, 'product' );
$this->seed_meta( 1, '_price', '99.99' );
$this->seed_meta( 1, '_regular_price', '120.00' );
$this->seed_meta( 1, '_stock', '5' );
$this->seed_meta( 1, '_stock_status', 'instock' );
$this->seed_meta( 1, '_sku', 'SKU-001' );
$this->seed_post( 2, 'product' );
$this->seed_meta( 2, '_price', '49.50' );
$this->seed_meta( 2, '_stock_status', 'outofstock' );
$result = WPDO_Post_Migration::backfill_group( 'wc_product' );
$this->assertSame( 2, $result['migrated'], 'Two posts produce two flat rows.' );
global $wpdb;
$row1 = $wpdb->get_row( 'SELECT _price, _stock, _sku FROM `' . self::FLAT . '` WHERE post_id = 1', ARRAY_A );
$this->assertSame( '99.990000', $row1['_price'], 'Price decimal stored with full precision.' );
$this->assertSame( '5', $row1['_stock'] );
$this->assertSame( 'SKU-001', $row1['_sku'] );
$row2 = $wpdb->get_row( 'SELECT _price, _stock_status, _sku FROM `' . self::FLAT . '` WHERE post_id = 2', ARRAY_A );
$this->assertSame( '49.500000', $row2['_price'] );
$this->assertSame( 'outofstock', $row2['_stock_status'] );
$this->assertNull( $row2['_sku'], 'Unset key remains NULL in flat row.' );
}
public function test_backfill_group_is_idempotent(): void {
$this->seed_post( 1, 'product' );
$this->seed_meta( 1, '_price', '50.00' );
WPDO_Post_Migration::backfill_group( 'wc_product' );
$result_second = WPDO_Post_Migration::backfill_group( 'wc_product' );
$this->assertSame( 1, $result_second['migrated'], 'Re-running backfill is idempotent (UPSERT).' );
global $wpdb;
$count = (int) $wpdb->get_var( 'SELECT COUNT(*) FROM `' . self::FLAT . '`' );
$this->assertSame( 1, $count, 'No duplicate rows after re-run.' );
}
public function test_backfill_group_skips_posts_of_wrong_type(): void {
// _price on a non-product post should NOT migrate to wc_product flat.
$this->seed_post( 99, 'post' );
$this->seed_meta( 99, '_price', '100.00' );
$result = WPDO_Post_Migration::backfill_group( 'wc_product' );
$this->assertSame( 0, $result['migrated'], 'Posts of wrong type are excluded by post_type filter.' );
}
public function test_backfill_group_rejects_invalid_group(): void {
$this->expectException( InvalidArgumentException::class );
WPDO_Post_Migration::backfill_group( 'bogus_group' );
}
}