Files
2meet-data-optimizer/tests/integration/DriftCheckTest.php
T
wpdev 3a347269c8 feat(2meet-data-optimizer): repair drifted flat rows from native with drift-check --fix
- TMDO_Drift_Check::fix() deletes flat rows of deleted entities and, for post groups, of posts outside the group's post types; rebuilds every other drifted entity's row from native through the new TMDO_Entity_Migration_Engine::migrate_entity() (keys without native rows become NULL, PR-066); keeps a row whose entity has no native value for the group but clears its columns (no NULL-only inserts, no deleted rows the router may join on); invalidates the row cache of each touched entity; then checks again. Native meta is never written.
- `wp tmdo drift-check <entity> --fix` (not combinable with --sample).
- Verify: DriftCheckTest fix cases (RED: method missing); dev36 --fix: post 42 rows rebuilt, 1 stray row (post 312, hp_booking in hp_draft_flags) deleted, all four entities 0 drift after, checker miss/extra 0.
- PR: PR-071
2026-09-27 00:07:33 +08:00

189 lines
8.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* TMDO_Drift_Check (2.2 PR-070): read-only flat-vs-native comparison.
*
* Fixture (user entity, group dc_grp):
* user 1 every key equal in value, differently formatted
* (50 vs 50.000000, serialized array vs JSON, required flag holding
* its default with no native row) → clean
* user 2 name 'b' vs 'B', cnt 5 vs 6 → diff ×2
* user 3 native name, flat row with name NULL → miss
* user 4 no native rows, flat cnt 9 → extra
* user 5 native name '', flat NULL → clean
* user 6 native name longer than a text column, flat NULL → clean
* user 7 native cnt, no flat row → miss
*/
class DriftCheckTest extends TestCase {
private const TABLE = 'wp_itest_wpdo_user_dc_grp';
public static function setUpBeforeClass(): void {
foreach ( array(
'includes/adapters/interface-entity-adapter.php',
'includes/adapters/class-tmdo-adapter-user.php',
'includes/diagnostic/class-tmdo-drift-check.php',
) as $file ) {
require_once WPDO_PLUGIN_DIR . $file;
}
}
protected function setUp(): void {
global $wpdb;
self::drop();
$wpdb->query( "CREATE TABLE `{$wpdb->users}` ( ID BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_login VARCHAR(60) NOT NULL DEFAULT '', PRIMARY KEY (ID) ) ENGINE=InnoDB" );
$wpdb->query( "CREATE TABLE `{$wpdb->usermeta}` ( umeta_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL DEFAULT 0, meta_key VARCHAR(255) DEFAULT NULL, meta_value LONGTEXT, PRIMARY KEY (umeta_id), KEY user_id (user_id) ) ENGINE=InnoDB" );
$wpdb->query( 'CREATE TABLE `' . self::TABLE . '` ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, num DECIMAL(18,6) DEFAULT NULL, cnt BIGINT(20) DEFAULT NULL, name VARCHAR(255) DEFAULT NULL, prefs TEXT DEFAULT NULL, meta_json LONGTEXT DEFAULT NULL, flag BIGINT(20) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_entity (user_id) ) ENGINE=InnoDB' );
for ( $id = 1; $id <= 7; $id++ ) {
$wpdb->insert( $wpdb->users, array( 'ID' => $id, 'user_login' => "u{$id}" ) );
}
$prefs = serialize( array( 'core' => array( 'welcomeGuide' => false ) ) );
$this->native( 1, array( 'num' => '50', 'cnt' => '3', 'name' => 'a', 'prefs' => $prefs, 'meta_json' => serialize( array( 'w' => 640, 'tags' => array( 'x' ) ) ) ) );
$this->flat( array( 'user_id' => 1, 'num' => '50.000000', 'cnt' => 3, 'name' => 'a', 'prefs' => $prefs, 'meta_json' => '{"w":640,"tags":["x"]}' ) );
$this->native( 2, array( 'name' => 'b', 'cnt' => '5' ) );
$this->flat( array( 'user_id' => 2, 'name' => 'B', 'cnt' => 6 ) );
$this->native( 3, array( 'name' => 'c' ) );
$this->flat( array( 'user_id' => 3, 'cnt' => null ) );
$this->flat( array( 'user_id' => 4, 'cnt' => 9 ) );
$this->native( 5, array( 'name' => '' ) );
$this->flat( array( 'user_id' => 5 ) );
$this->native( 6, array( 'name' => str_repeat( 'x', 300 ) ) );
$this->flat( array( 'user_id' => 6 ) );
$this->native( 7, array( 'cnt' => '7' ) );
$GLOBALS['_wp_options'] = array();
$GLOBALS['_wp_cache'] = array();
WPDO_Entity_Registry::init();
WPDO_Entity_Registry::register_adapter( 'user', new WPDO_Adapter_User() );
WPDO_Entity_Registry::register_group(
'user',
'dc_grp',
array(
array( 'key' => 'num', 'type' => 'decimal' ),
array( 'key' => 'cnt', 'type' => 'integer' ),
array( 'key' => 'name', 'type' => 'text' ),
array( 'key' => 'prefs', 'type' => 'textarea' ),
array( 'key' => 'meta_json', 'type' => 'json' ),
array( 'key' => 'flag', 'type' => 'integer', 'default' => 0, 'required' => true ),
)
);
WPDO_Schema_Manager::flush_table_exists_cache();
}
protected function tearDown(): void {
self::drop();
WPDO_Entity_Registry::init();
}
private static function drop(): void {
global $wpdb;
foreach ( array( $wpdb->users, $wpdb->usermeta, self::TABLE ) as $table ) {
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
}
}
private function native( int $user_id, array $values ): void {
global $wpdb;
foreach ( $values as $key => $value ) {
$wpdb->insert( $wpdb->usermeta, array( 'user_id' => $user_id, 'meta_key' => $key, 'meta_value' => $value ) );
}
}
private function flat( array $row ): void {
global $wpdb;
$wpdb->insert( self::TABLE, $row );
}
private function snapshot(): string {
global $wpdb;
return md5( wp_json_encode( array( $wpdb->get_results( "SELECT * FROM `{$wpdb->usermeta}` ORDER BY umeta_id", ARRAY_A ), $wpdb->get_results( 'SELECT * FROM `' . self::TABLE . '` ORDER BY id', ARRAY_A ) ) ) );
}
public function test_counts_miss_diff_and_extra_per_key(): void {
$report = TMDO_Drift_Check::run( 'user' );
$this->assertSame( array( 'miss' => 2, 'diff' => 2, 'extra' => 1 ), $report['total'] );
$keys = $report['groups']['dc_grp']['keys'];
$this->assertSame( array( 'miss' => 1, 'diff' => 1, 'extra' => 0 ), $keys['name'] );
$this->assertSame( array( 'miss' => 1, 'diff' => 1, 'extra' => 1 ), $keys['cnt'] );
foreach ( array( 'num', 'prefs', 'meta_json', 'flag' ) as $key ) {
$this->assertSame( array( 'miss' => 0, 'diff' => 0, 'extra' => 0 ), $keys[ $key ], "{$key}: equal by value" );
}
}
public function test_records_the_outcome_per_group(): void {
TMDO_Drift_Check::run( 'user', 'dc_grp' );
$status = get_option( TMDO_Drift_Check::OPTION )['user']['dc_grp'];
$this->assertSame( 2, $status['miss'] );
$this->assertSame( 2, $status['diff'] );
$this->assertSame( 1, $status['extra'] );
$this->assertSame( 0, $status['sample'] );
$this->assertNotEmpty( $status['checked_at'] );
}
public function test_a_sample_covering_every_user_matches_the_full_check(): void {
$report = TMDO_Drift_Check::run( 'user', '', 100 );
$this->assertSame( array( 'miss' => 2, 'diff' => 2, 'extra' => 1 ), $report['total'] );
$this->assertSame( 100, get_option( TMDO_Drift_Check::OPTION )['user']['dc_grp']['sample'] );
}
public function test_never_writes_native_or_flat(): void {
$before = $this->snapshot();
TMDO_Drift_Check::run( 'user' );
$this->assertSame( $before, $this->snapshot() );
}
// ── fix() (2.2 PR-071) ──────────────────────────────────────────────────
private function flat_row( int $user_id ): ?array {
global $wpdb;
return $wpdb->get_row( $wpdb->prepare( 'SELECT num, cnt, name, flag FROM `' . self::TABLE . '` WHERE user_id = %d', $user_id ), ARRAY_A );
}
public function test_fix_rebuilds_drifted_rows_from_native_and_leaves_no_drift(): void {
$result = TMDO_Drift_Check::fix( 'user' );
$this->assertSame( array( 'miss' => 0, 'diff' => 0, 'extra' => 0 ), $result['after']['total'] );
$this->assertSame( 'b', $this->flat_row( 2 )['name'] );
$this->assertSame( '5', $this->flat_row( 2 )['cnt'] );
$this->assertSame( 'c', $this->flat_row( 3 )['name'] );
$this->assertSame( array( 'num' => null, 'cnt' => '7', 'name' => null, 'flag' => '0' ), $this->flat_row( 7 ), 'new row: no invented defaults' );
$this->assertSame( array( 'num' => null, 'cnt' => null, 'name' => null, 'flag' => '0' ), $this->flat_row( 4 ), 'no native values: row kept, columns cleared' );
$this->assertSame( 0, get_option( TMDO_Drift_Check::OPTION )['user']['dc_grp']['diff'] );
}
public function test_fix_deletes_rows_of_entities_that_no_longer_exist(): void {
$this->flat( array( 'user_id' => 99, 'name' => 'ghost' ) );
$result = TMDO_Drift_Check::fix( 'user' );
$this->assertSame( 1, $result['fixed']['deleted_rows'] );
$this->assertNull( $this->flat_row( 99 ) );
$this->assertNotNull( $this->flat_row( 1 ), 'rows of existing users stay' );
}
public function test_fix_never_writes_native_and_invalidates_the_row_cache(): void {
global $wpdb;
$native = md5( wp_json_encode( $wpdb->get_results( "SELECT * FROM `{$wpdb->usermeta}` ORDER BY umeta_id", ARRAY_A ) ) );
TMDO_Cache_Orchestrator::set_row( 'user', 2, 'dc_grp', array( 'name' => 'B' ) );
TMDO_Drift_Check::fix( 'user' );
$this->assertSame( $native, md5( wp_json_encode( $wpdb->get_results( "SELECT * FROM `{$wpdb->usermeta}` ORDER BY umeta_id", ARRAY_A ) ) ) );
$this->assertFalse( TMDO_Cache_Orchestrator::get_row( 'user', 2, 'dc_grp' ) );
}
public function test_rejects_an_unknown_group(): void {
$this->expectException( InvalidArgumentException::class );
TMDO_Drift_Check::run( 'user', 'no_such_group' );
}
}