Files
2meet-data-optimizer/tests/integration/DriftCheckTest.php
T
wpdev 6c782282c7 feat(2meet-data-optimizer): sample drift daily and record the result
- TMDO_Drift_Check::cron_tick() runs on the existing wpdo_daily_health_check event (no new schedule to add or clear): every entity type whose flat tables are written (dual_write or higher) is checked on 500 random entities per group, and the outcome replaces the recorded status, so a group found drifting leaves the router that day.
- Verify: DriftCheckTest cron cases (RED: method / hook missing); dev36 with all four entities in dual_write: one tick over 22 groups in 10 s, 0 drift, status option restored.
- PR: PR-073
2026-09-27 00:28:00 +08:00

228 lines
10 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' ) );
}
// ── cron_tick() (2.2 PR-073) ────────────────────────────────────────────
private static function set_modes( ?array $modes ): void {
$cache = ( new ReflectionClass( WPDO_Mode_Manager::class ) )->getProperty( 'cache' );
$cache->setAccessible( true );
$cache->setValue( null, $modes );
}
public function test_the_daily_tick_samples_entities_that_write_to_flat(): void {
self::set_modes( array( 'post' => 'disabled', 'user' => 'dual_write', 'term' => 'disabled', 'comment' => 'disabled' ) );
try {
TMDO_Drift_Check::cron_tick();
} finally {
self::set_modes( null );
}
$status = get_option( TMDO_Drift_Check::OPTION );
$this->assertSame( TMDO_Drift_Check::CRON_SAMPLE, $status['user']['dc_grp']['sample'] );
$this->assertSame( 2, $status['user']['dc_grp']['diff'], 'the sample covers all 7 fixture users' );
$this->assertArrayNotHasKey( 'post', $status );
}
public function test_the_daily_tick_skips_entities_that_do_not_write_to_flat(): void {
self::set_modes( array( 'post' => 'disabled', 'user' => 'disabled', 'term' => 'disabled', 'comment' => 'disabled' ) );
try {
TMDO_Drift_Check::cron_tick();
} finally {
self::set_modes( null );
}
$this->assertFalse( get_option( TMDO_Drift_Check::OPTION ) );
}
public function test_the_daily_tick_runs_on_the_daily_health_check_hook(): void {
$source = (string) file_get_contents( WPDO_PLUGIN_DIR . 'includes/class-tmdo-core.php' );
$this->assertStringContainsString( "add_action( 'wpdo_daily_health_check', array( 'TMDO_Drift_Check', 'cron_tick' ) );", $source );
}
public function test_rejects_an_unknown_group(): void {
$this->expectException( InvalidArgumentException::class );
TMDO_Drift_Check::run( 'user', 'no_such_group' );
}
}