d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
331 lines
11 KiB
PHP
331 lines
11 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Integration test: WPDO_Post_Shadow_Verifier (v2.10.3).
|
|
*
|
|
* Verifies sample-and-compare logic between flat tables and wp_postmeta.
|
|
* Result tuple: {sampled, matched, diffs, missing_flat, missing_postmeta}.
|
|
*
|
|
* Logger integration is exercised via WPDO_Shadow_Diff_Logger; this test
|
|
* focuses on the verifier's sampling + counting contract.
|
|
*/
|
|
class PostShadowVerifierTest 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;
|
|
|
|
if ( ! interface_exists( 'WPDO_Entity_Adapter_Interface' ) ) {
|
|
require_once WPDO_PLUGIN_DIR . 'includes/adapters/interface-entity-adapter.php';
|
|
}
|
|
foreach ( array(
|
|
'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/class-tmdo-post-shadow-verifier.php',
|
|
) as $rel ) {
|
|
$file = WPDO_PLUGIN_DIR . $rel;
|
|
if ( file_exists( $file ) ) {
|
|
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_type varchar(20) NOT NULL DEFAULT \'post\',
|
|
post_status varchar(20) NOT NULL DEFAULT \'publish\',
|
|
PRIMARY KEY (ID),
|
|
KEY post_type (post_type)
|
|
) 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'
|
|
);
|
|
|
|
$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,
|
|
_stock_status varchar(100) DEFAULT NULL,
|
|
_sku longtext DEFAULT NULL,
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY uk_post_id (post_id)
|
|
) DEFAULT CHARACTER SET utf8mb4'
|
|
);
|
|
|
|
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 . '`' );
|
|
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 . '`' );
|
|
}
|
|
|
|
private function seed_consistent( int $post_id, string $price, string $stock ): void {
|
|
global $wpdb;
|
|
$wpdb->insert( self::POSTS, array( 'ID' => $post_id, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_price', 'meta_value' => $price ) );
|
|
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_stock_status', 'meta_value' => $stock ) );
|
|
$wpdb->insert( self::FLAT, array( 'post_id' => $post_id, '_price' => $price, '_stock_status' => $stock ) );
|
|
}
|
|
|
|
private function seed_diverged( int $post_id, string $pm_price, string $flat_price ): void {
|
|
global $wpdb;
|
|
$wpdb->insert( self::POSTS, array( 'ID' => $post_id, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::POSTMETA, array( 'post_id' => $post_id, 'meta_key' => '_price', 'meta_value' => $pm_price ) );
|
|
$wpdb->insert( self::FLAT, array( 'post_id' => $post_id, '_price' => $flat_price ) );
|
|
}
|
|
|
|
// ── sample_compare() ─────────────────────────────────────────────────────
|
|
|
|
public function test_returns_expected_shape(): void {
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price', '_stock_status' ),
|
|
5
|
|
);
|
|
|
|
$this->assertArrayHasKey( 'sampled', $result );
|
|
$this->assertArrayHasKey( 'matched', $result );
|
|
$this->assertArrayHasKey( 'diffs', $result );
|
|
$this->assertArrayHasKey( 'missing_flat', $result );
|
|
$this->assertArrayHasKey( 'missing_postmeta', $result );
|
|
}
|
|
|
|
public function test_all_match_when_data_is_consistent(): void {
|
|
for ( $i = 1; $i <= 5; $i++ ) {
|
|
$this->seed_consistent( $i, '50.00', 'instock' );
|
|
}
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 5, $result['sampled'] );
|
|
$this->assertSame( 5, $result['matched'] );
|
|
$this->assertSame( 0, $result['diffs'] );
|
|
$this->assertSame( 0, $result['missing_flat'] );
|
|
}
|
|
|
|
public function test_detects_divergence_between_flat_and_postmeta(): void {
|
|
// Two diverged: pm has 50, flat has 60.
|
|
$this->seed_diverged( 1, '50', '60' );
|
|
$this->seed_diverged( 2, '99', '88' );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 2, $result['sampled'] );
|
|
$this->assertSame( 0, $result['matched'] );
|
|
$this->assertSame( 2, $result['diffs'] );
|
|
}
|
|
|
|
public function test_counts_missing_flat_when_flat_row_absent(): void {
|
|
// Post + postmeta exist, but no flat row.
|
|
global $wpdb;
|
|
$wpdb->insert( self::POSTS, array( 'ID' => 100, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::POSTMETA, array( 'post_id' => 100, 'meta_key' => '_price', 'meta_value' => '99' ) );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 1, $result['sampled'] );
|
|
$this->assertSame( 1, $result['missing_flat'] );
|
|
}
|
|
|
|
public function test_counts_missing_postmeta_when_pm_absent(): void {
|
|
// Post + flat exist, but no postmeta.
|
|
global $wpdb;
|
|
$wpdb->insert( self::POSTS, array( 'ID' => 200, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::FLAT, array( 'post_id' => 200, '_price' => '50' ) );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 1, $result['sampled'] );
|
|
$this->assertSame( 1, $result['missing_postmeta'] );
|
|
}
|
|
|
|
public function test_returns_zero_when_no_posts_of_type(): void {
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 0, $result['sampled'] );
|
|
$this->assertSame( 0, $result['matched'] );
|
|
}
|
|
|
|
public function test_caps_sample_at_available_post_count(): void {
|
|
// 3 posts, ask for 10 samples → only 3 sampled.
|
|
$this->seed_consistent( 1, '10', 'instock' );
|
|
$this->seed_consistent( 2, '20', 'instock' );
|
|
$this->seed_consistent( 3, '30', 'instock' );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_price' ),
|
|
10
|
|
);
|
|
|
|
$this->assertSame( 3, $result['sampled'] );
|
|
$this->assertSame( 3, $result['matched'] );
|
|
}
|
|
|
|
public function test_rejects_zero_sample_size(): void {
|
|
$this->expectException( InvalidArgumentException::class );
|
|
WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array( '_price' ), 0 );
|
|
}
|
|
|
|
public function test_rejects_empty_keys(): void {
|
|
$this->expectException( InvalidArgumentException::class );
|
|
WPDO_Post_Shadow_Verifier::sample_compare( 'product', 'wc_product', self::FLAT, array(), 5 );
|
|
}
|
|
|
|
// ── v2.10.5: serialize vs JSON loose equality ────────────────────────────
|
|
|
|
public function test_treats_serialized_array_equal_to_json_array(): void {
|
|
// pm side has serialized array; flat side has JSON for the same data.
|
|
// post_id=10, key=_stock_status (we reuse this column to inject test values).
|
|
// Use a dedicated key for clarity by re-purposing the keys array.
|
|
global $wpdb;
|
|
$wpdb->insert( self::POSTS, array( 'ID' => 10, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::POSTMETA, array(
|
|
'post_id' => 10,
|
|
'meta_key' => '_sku',
|
|
'meta_value' => serialize( array( 'a', 'b', 'c' ) ),
|
|
) );
|
|
$wpdb->insert( self::FLAT, array(
|
|
'post_id' => 10,
|
|
'_sku' => wp_json_encode( array( 'a', 'b', 'c' ) ),
|
|
) );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_sku' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame(
|
|
1,
|
|
$result['matched'],
|
|
'serialized array vs JSON-encoded same array should match.'
|
|
);
|
|
$this->assertSame( 0, $result['diffs'], 'No false-positive diff.' );
|
|
}
|
|
|
|
public function test_treats_serialized_assoc_equal_to_json_assoc(): void {
|
|
global $wpdb;
|
|
$assoc = array( 'width' => 100, 'height' => 200 );
|
|
$wpdb->insert( self::POSTS, array( 'ID' => 11, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::POSTMETA, array(
|
|
'post_id' => 11,
|
|
'meta_key' => '_sku',
|
|
'meta_value' => serialize( $assoc ),
|
|
) );
|
|
$wpdb->insert( self::FLAT, array(
|
|
'post_id' => 11,
|
|
'_sku' => wp_json_encode( $assoc ),
|
|
) );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_sku' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 1, $result['matched'] );
|
|
$this->assertSame( 0, $result['diffs'] );
|
|
}
|
|
|
|
public function test_genuine_diff_still_detected_after_loose_equal_widening(): void {
|
|
// Real divergence — must still be flagged even with loosened compare.
|
|
global $wpdb;
|
|
$wpdb->insert( self::POSTS, array( 'ID' => 12, 'post_type' => 'product', 'post_status' => 'publish' ) );
|
|
$wpdb->insert( self::POSTMETA, array(
|
|
'post_id' => 12,
|
|
'meta_key' => '_sku',
|
|
'meta_value' => serialize( array( 1, 2, 3 ) ),
|
|
) );
|
|
$wpdb->insert( self::FLAT, array(
|
|
'post_id' => 12,
|
|
'_sku' => wp_json_encode( array( 9, 9, 9 ) ),
|
|
) );
|
|
|
|
$result = WPDO_Post_Shadow_Verifier::sample_compare(
|
|
'product',
|
|
'wc_product',
|
|
self::FLAT,
|
|
array( '_sku' ),
|
|
5
|
|
);
|
|
|
|
$this->assertSame( 0, $result['matched'] );
|
|
$this->assertSame( 1, $result['diffs'] );
|
|
}
|
|
}
|