getProperty( 'instance' )->setValue( null, null ); // The cold module is write-active unless a test says otherwise. $this->set_cold_module( 'dual_write' ); WPDO_Schema_Manager::flush_table_exists_cache(); $this->setup_wpdb_mock(); } private function set_cold_module( string $state ): void { $GLOBALS['_wp_options']['wpdo_features'] = array( 'cold_hp_listing' => $state ); $prop = new ReflectionProperty( WPDO_Feature_Flags::class, 'cache' ); $prop->setValue( null, null ); } private function setup_wpdb_mock(): void { global $wpdb; $wpdb = new class { public string $prefix = 'wp_'; public function prepare( string $sql, ...$args ): string { $i = 0; return preg_replace_callback( '/%([sd])/', function ( $m ) use ( &$i, $args ) { $val = $args[ $i++ ] ?? ''; return $m[1] === 'd' ? (string) (int) $val : "'" . addslashes( (string) $val ) . "'"; }, $sql ); } public function get_var( string $sql ): ?string { if ( preg_match( "/^SHOW TABLES LIKE '(.+)'$/", $sql, $m ) ) { return CacheLayerTest::$table_exists ? stripslashes( $m[1] ) : null; } if ( str_contains( $sql, 'wpdo_cold_' ) ) { ++CacheLayerTest::$cold_queries; } return null; } public function get_row( string $sql, $output = OBJECT ) { return null; } public function insert( string $table, array $data, $format = null ): int|false { return 1; } public function update( string $table, array $data, array $where, $f = null, $wf = null ): int|false { return 1; } public function delete( string $table, array $where, $format = null ): int|false { return 1; } public function query( string $sql ): int|bool { return 1; } public function get_results( string $sql, $output = OBJECT ): array { if ( str_contains( $sql, 'wpdo_cold_' ) ) { ++CacheLayerTest::$cold_queries; } return CacheLayerTest::$db_rows; } }; } // ── Zero side effect while the cold module is not active ───────────────── public function test_prefetch_does_not_query_while_module_idle(): void { $this->register_cold_field(); $this->set_cold_module( 'idle' ); WPDO_Cache_Layer::prefetch( [ 1, 2 ], 'hp_listing' ); $this->assertSame( 0, self::$cold_queries ); $this->assertEmpty( $GLOBALS['_wp_cache'] ); } public function test_warm_post_does_not_query_while_module_idle(): void { $this->register_cold_field(); $this->set_cold_module( 'idle' ); WPDO_Cache_Layer::warm_post( 20, 'hp_listing' ); $this->assertSame( 0, self::$cold_queries ); } public function test_no_query_when_cold_table_is_missing(): void { $this->register_cold_field(); self::$table_exists = false; WPDO_Cache_Layer::prefetch( [ 1 ], 'hp_listing' ); WPDO_Cache_Layer::warm_post( 1, 'hp_listing' ); $this->assertSame( 0, self::$cold_queries ); } // ── Helper: register cold field ────────────────────────────────────────── private function register_cold_field( string $post_type = 'hp_listing', string $meta_key = 'hp_description' ): void { WPDO_Schema_Registry::instance()->register( 'test', [ 'post_type' => $post_type, 'meta_key' => $meta_key, 'zone' => 'cold', ] ); } // ── prefetch() ─────────────────────────────────────────────────────────── public function test_prefetch_returns_early_for_empty_post_ids(): void { $this->register_cold_field(); WPDO_Cache_Layer::prefetch( [], 'hp_listing' ); $this->assertEmpty( $GLOBALS['_wp_cache'] ); } public function test_prefetch_returns_early_when_no_cold_keys_for_type(): void { // 'unknown_type' has no registered cold fields. WPDO_Cache_Layer::prefetch( [ 1, 2, 3 ], 'unknown_type' ); $this->assertEmpty( $GLOBALS['_wp_cache'] ); } public function test_prefetch_skips_already_cached_post_ids(): void { $this->register_cold_field(); $group = 'wpdo_cold_hp_listing'; // Pre-warm cache for post 1. $GLOBALS['_wp_cache'][ $group ]['cold_1'] = [ 'hp_description' => 'cached' ]; // DB returns no extra rows — all were already cached. self::$db_rows = []; WPDO_Cache_Layer::prefetch( [ 1 ], 'hp_listing' ); // Cache should remain unchanged (no new entry written). $this->assertSame( [ 'hp_description' => 'cached' ], $GLOBALS['_wp_cache'][ $group ]['cold_1'] ); } public function test_prefetch_stores_fetched_data_in_cache(): void { $this->register_cold_field(); self::$db_rows = [ [ 'post_id' => '5', 'data' => json_encode( [ 'hp_description' => 'Fetched!' ] ) ], ]; WPDO_Cache_Layer::prefetch( [ 5 ], 'hp_listing' ); $group = 'wpdo_cold_hp_listing'; $cached = $GLOBALS['_wp_cache'][ $group ]['cold_5'] ?? false; $this->assertIsArray( $cached ); $this->assertSame( 'Fetched!', $cached['hp_description'] ); } public function test_prefetch_stores_empty_array_for_post_with_no_cold_row(): void { $this->register_cold_field(); // DB returns nothing for post 7. self::$db_rows = []; WPDO_Cache_Layer::prefetch( [ 7 ], 'hp_listing' ); $group = 'wpdo_cold_hp_listing'; $cached = $GLOBALS['_wp_cache'][ $group ]['cold_7'] ?? 'NOT_SET'; $this->assertSame( [], $cached ); } // ── warm_post() ────────────────────────────────────────────────────────── public function test_warm_post_returns_early_when_no_cold_keys(): void { // 'no_cold_type' has no cold fields → warm_post returns immediately. WPDO_Cache_Layer::warm_post( 1, 'no_cold_type' ); $this->assertEmpty( $GLOBALS['_wp_cache'] ); } public function test_warm_post_clears_stale_cache_entry(): void { $this->register_cold_field(); $group = 'wpdo_cold_hp_listing'; $cache_key = 'cold_20'; // Pre-populate with stale data. $GLOBALS['_wp_cache'][ $group ][ $cache_key ] = [ 'stale' => true ]; WPDO_Cache_Layer::warm_post( 20, 'hp_listing' ); // Stale cache must be replaced (warm_post deletes then re-reads from DB, // which returns null in mock, yielding empty array). $this->assertNotSame( [ 'stale' => true ], $GLOBALS['_wp_cache'][ $group ][ $cache_key ] ?? null ); } /** * Flushing bumps the post type's cache version instead of asking the * object cache to scan the group (a server-side script on Redis that * blocks it on a large group); older entries expire by their TTL. */ public function test_flush_group_moves_to_new_keys_without_a_group_flush(): void { $group = 'wpdo_cold_hp_listing'; $old_key = WPDO_Zone_Cold::cache_key( 1, 'hp_listing' ); $GLOBALS['_wp_cache'][ $group ][ $old_key ] = array( 'stale' => true ); WPDO_Cache_Layer::flush_group( 'hp_listing' ); $new_key = WPDO_Zone_Cold::cache_key( 1, 'hp_listing' ); $this->assertNotSame( $old_key, $new_key ); $this->assertFalse( wp_cache_get( $new_key, $group ) ); $this->assertSame( 'cold_2', WPDO_Zone_Cold::cache_key( 2, 'other_type' ), 'other post types keep their keys' ); $this->assertArrayHasKey( $old_key, $GLOBALS['_wp_cache'][ $group ], 'nothing scanned or deleted' ); } // ── get_stats() ────────────────────────────────────────────────────────── public function test_get_stats_includes_required_keys(): void { $stats = WPDO_Cache_Layer::get_stats(); $this->assertArrayHasKey( 'groups', $stats ); $this->assertArrayHasKey( 'prefetch_support', $stats ); $this->assertArrayHasKey( 'flush_support', $stats ); } public function test_get_stats_groups_reflect_registered_cold_types(): void { $this->register_cold_field( 'hp_listing', 'hp_description' ); $this->register_cold_field( 'hp_listing', 'hp_website' ); $stats = WPDO_Cache_Layer::get_stats(); $post_types = array_column( $stats['groups'], 'post_type' ); $this->assertContains( 'hp_listing', $post_types ); } }