original_wpdb = $wpdb; $GLOBALS['_wp_options'] = array(); WPDO_Mode_Manager::reset_cache(); // Provide a fresh $wpdb with all methods the phases under test require, // regardless of which prior test may have left a broken stub. $wpdb = new class { public string $prefix = 'wp_'; public string $users = 'wp_users'; public string $usermeta = 'wp_usermeta'; public string $postmeta = 'wp_postmeta'; public string $options = 'wp_options'; public string $last_error = ''; public array $queries = []; public function prepare( string $sql, ...$args ): string { $i = 0; return preg_replace_callback( '/%[sd]/', function () use ( &$i, $args ) { return (string) ( $args[ $i++ ] ?? '?' ); }, $sql ); } public function get_var( string $sql ): ?string { return null; } public function get_col( string $sql ): array { return []; } public function get_results( string $sql, $output = null ): array { return []; } public function query( string $sql ): int|bool { $this->queries[] = $sql; return 1; } public function insert( string $t, array $d, $f = null ): int|false { return 1; } public function get_charset_collate(): string { return ''; } }; // Reset Schema_Manager request-scoped cache so table_exists() hits $wpdb each test. $ref = new ReflectionClass( WPDO_Schema_Manager::class ); $prop = $ref->getProperty( 'table_exists_cache' ); $prop->setAccessible( true ); $prop->setValue( null, array() ); // The unit bootstrap loads Member_Fields / Post_Fields, which register entity // groups at include time. These phases assert "no groups registered" behaviour, // so clear the registry to isolate them from that ambient state. $reg = new ReflectionClass( WPDO_Entity_Registry::class ); foreach ( array( 'groups', 'field_index' ) as $name ) { $p = $reg->getProperty( $name ); $p->setAccessible( true ); $p->setValue( null, array() ); } // Install_Schema re-fires wpdo_register_entity_fields, which would re-populate // the registry from those same listeners — drop them for the duration. $this->saved_field_listeners = $GLOBALS['_wp_filter_callbacks']['wpdo_register_entity_fields'] ?? array(); $GLOBALS['_wp_filter_callbacks']['wpdo_register_entity_fields'] = array(); WPDO_Entity_Registry::clear_pending_schemas(); } /** * Listeners removed in setUp() and restored in tearDown(). * * @var array */ private array $saved_field_listeners = array(); protected function tearDown(): void { global $wpdb; $wpdb = $this->original_wpdb; $GLOBALS['_wp_filter_callbacks']['wpdo_register_entity_fields'] = $this->saved_field_listeners; } private function make_job(): array { return array( 'job_id' => 'test-job', 'log' => array(), 'state' => 'running', 'overall_progress' => 0, 'metrics' => array( 'ratio_start' => 1.5, 'ratio_now' => 1.5, ), 'options' => array(), ); } // ── WPDO_Phase_Diagnose ─────────────────────────────────────────────────── public function test_diagnose_name(): void { $phase = new WPDO_Phase_Diagnose( 'user' ); $this->assertSame( 'diagnose', $phase->name() ); } public function test_diagnose_returns_ok(): void { $phase = new WPDO_Phase_Diagnose( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); } public function test_diagnose_records_eav_rows_in_metrics(): void { // With $wpdb returning null, count_eav_residue() → 0. Key must exist. $phase = new WPDO_Phase_Diagnose( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertArrayHasKey( 'eav_rows_now', $job['metrics'] ); $this->assertIsInt( $job['metrics']['eav_rows_now'] ); } public function test_diagnose_records_ratio_in_metrics(): void { // 0 users → ratio = 0. Key must exist. $phase = new WPDO_Phase_Diagnose( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertArrayHasKey( 'ratio_now', $job['metrics'] ); } public function test_diagnose_appends_log_entry(): void { $phase = new WPDO_Phase_Diagnose( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertNotEmpty( $job['log'] ); $this->assertStringContainsString( 'Diagnose', $job['log'][0] ); } // ── WPDO_Phase_Install_Schema ───────────────────────────────────────────── public function test_install_schema_name(): void { $phase = new WPDO_Phase_Install_Schema( 'user' ); $this->assertSame( 'install_schema', $phase->name() ); } public function test_install_schema_succeeds_when_no_groups_registered(): void { // No groups registered for 'user' in unit-test bootstrap → $missing stays empty. $phase = new WPDO_Phase_Install_Schema( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); } public function test_install_schema_appends_log_entry(): void { $phase = new WPDO_Phase_Install_Schema( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertNotEmpty( $job['log'] ); $this->assertStringContainsString( 'Schema migration ok', $job['log'][0] ); } // ── WPDO_Phase_Backfill_Bulk ────────────────────────────────────────────── public function test_backfill_bulk_name(): void { $phase = new WPDO_Phase_Backfill_Bulk( 'user' ); $this->assertSame( 'backfill_bulk', $phase->name() ); } public function test_backfill_bulk_skips_on_dry_run(): void { $phase = new WPDO_Phase_Backfill_Bulk( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'dry_run' => true ) ) ); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertStringContainsString( 'dry_run', $job['log'][0] ?? '' ); } public function test_backfill_bulk_returns_ok_with_no_groups(): void { // No groups → loop body never executes → 0 groups, 0 rows. $phase = new WPDO_Phase_Backfill_Bulk( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $found_summary = false; foreach ( $job['log'] as $line ) { if ( str_contains( $line, 'Bulk backfill' ) ) { $found_summary = true; $this->assertStringContainsString( '0 groups', $line ); break; } } $this->assertTrue( $found_summary, 'Log must contain Bulk backfill summary.' ); } // ── WPDO_Phase_Backfill_Unserialize ─────────────────────────────────────── public function test_backfill_unserialize_name(): void { $phase = new WPDO_Phase_Backfill_Unserialize( 'user' ); $this->assertSame( 'backfill_unserialize', $phase->name() ); } public function test_backfill_unserialize_skips_on_dry_run(): void { $phase = new WPDO_Phase_Backfill_Unserialize( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'dry_run' => true ) ) ); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); $this->assertStringContainsString( 'dry_run', $job['log'][0] ?? '' ); } public function test_backfill_unserialize_returns_ok_with_no_json_groups(): void { // No groups with JSON fields → loop body never executes. $phase = new WPDO_Phase_Backfill_Unserialize( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); } public function test_backfill_unserialize_appends_total_log_entry(): void { $phase = new WPDO_Phase_Backfill_Unserialize( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $this->assertNotEmpty( $job['log'] ); $last = end( $job['log'] ); $this->assertStringContainsString( 'Row-by-row backfill', $last ); } // ── WPDO_Phase_Verify_Sample ────────────────────────────────────────────── public function test_verify_sample_name(): void { $phase = new WPDO_Phase_Verify_Sample( 'user' ); $this->assertSame( 'verify_sample', $phase->name() ); } public function test_verify_sample_24h_starts_window_when_no_started_at(): void { $phase = new WPDO_Phase_Verify_Sample( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'verify_24h' => true ) ) ); $result = $phase->execute( $job ); $this->assertSame( 'in_progress', $result['status'] ); $this->assertArrayHasKey( 'phase_started_at', $job ); } public function test_verify_sample_24h_stays_in_progress_during_window(): void { $phase = new WPDO_Phase_Verify_Sample( 'user' ); $job = array_merge( $this->make_job(), array( 'options' => array( 'verify_24h' => true ), 'phase_started_at' => time() - 3600, // 1 hour ago — still within 24h window ) ); $result = $phase->execute( $job ); $this->assertSame( 'in_progress', $result['status'] ); } public function test_verify_sample_succeeds_with_zero_users(): void { // $wpdb->get_var() returns null → 0 users → 0 samples → 0 diffs → ok. $phase = new WPDO_Phase_Verify_Sample( 'user' ); $job = $this->make_job(); $result = $phase->execute( $job ); $this->assertSame( 'ok', $result['status'] ); } public function test_verify_sample_log_contains_compare_summary(): void { $phase = new WPDO_Phase_Verify_Sample( 'user' ); $job = $this->make_job(); $phase->execute( $job ); $found = false; foreach ( $job['log'] as $line ) { if ( str_contains( $line, 'Verify' ) ) { $found = true; break; } } $this->assertTrue( $found, 'Log must contain Verify summary.' ); } public function test_verify_sample_constants(): void { $this->assertSame( 500, WPDO_Phase_Verify_Sample::VERIFY_SAMPLE_MIN ); $this->assertEqualsWithDelta( 0.10, WPDO_Phase_Verify_Sample::VERIFY_SAMPLE_RATIO, 0.0001 ); } }