Files
2meet-data-optimizer/tests/unit/CacheLayerTest.php
T
wpdev 20bd59ddae fix(2meet-data-optimizer): skip cold-zone cache warming unless the cold module is active
- TMDO_Cache_Layer::prefetch() / warm_post() (and so on_save_post /
  on_loop_start) queried wp_wpdo_cold_{post_type} whenever cold fields
  were registered, even with the cold module idle and the table never
  created: every HivePress / WC save logged "table doesn't exist" errors.
  They now return unless the module is write-active and the table exists.
- CacheLayerTest activates the module in setUp; new cases assert zero cold
  queries while idle or when the table is missing.
- Verify: CacheLayerTest 12 OK; unit 671 OK, integration 480 OK,
  phpcs (phpcs.xml) 0, stan OK; dev36 live: seeding listings/products/
  vendors with cold_hp_listing idle → 0 wpdo_cold errors (was hundreds)
- PR: PR-054
2026-09-26 18:34:09 +08:00

219 lines
7.8 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Unit tests for WPDO_Cache_Layer — Object Cache integration for Zone C (Cold).
*/
class CacheLayerTest extends TestCase {
/** Rows returned by get_results() for the bulk-fetch query. */
public static array $db_rows = [];
/** Whether SHOW TABLES reports the cold table. */
public static bool $table_exists = true;
/** Queries that read the cold table (get_var / get_results on it). */
public static int $cold_queries = 0;
protected function setUp(): void {
self::$db_rows = [];
self::$table_exists = true;
self::$cold_queries = 0;
$GLOBALS['_wp_cache'] = [];
// Reset Schema Registry singleton.
$ref = new ReflectionClass( WPDO_Schema_Registry::class );
$ref->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 );
}
// ── 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 );
}
}