151a14a7e8
- CREATE TABLE wpdo_warm 的 meta_key varchar(255)→varchar(191), KEY idx_post_meta → UNIQUE KEY ui_post_meta:沒有這個唯一鍵, ON DUPLICATE KEY UPDATE 不會觸發,會不斷 INSERT 重複列。 (既有站台的 ALTER 遷移原本就在 installer:1329,只有 CREATE 落後) - Zone_Warm::set() 由 SELECT→update/insert 改為 TMDO_DB::upsert() - 新增 Zone_Warm::increment():CAST(COALESCE(meta_value,0) AS SIGNED)+N 原子計數,取代 get()+set() 的 read-modify-write race(A v3.3.3) - ZoneWarmTest stub 與 ZoneWarmIntegrationTest 建表同步適配 unit 379 / integration 398 GREEN Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
218 lines
7.8 KiB
PHP
218 lines
7.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Tests for WPDO_Zone_Warm — Zone B KV table with TTL.
|
|
*
|
|
* Uses an in-memory array to simulate wpdb queries via a custom mock.
|
|
*/
|
|
class ZoneWarmTest extends TestCase {
|
|
|
|
/** In-memory warm store: post_id => meta_key => [value, expires_at] */
|
|
public static array $store = [];
|
|
|
|
protected function setUp(): void {
|
|
self::$store = [];
|
|
$this->setupWpdbMock();
|
|
}
|
|
|
|
private function setupWpdbMock(): 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 {
|
|
$store = &ZoneWarmTest::$store;
|
|
// Normalize whitespace so multiline SQL works with regex.
|
|
$flat = preg_replace( '/\s+/', ' ', $sql );
|
|
if ( preg_match( '/SELECT id.*post_id = (\d+).*meta_key = \'([^\']+)\'/', $flat, $m ) ) {
|
|
return isset( $store[ $m[1] ][ $m[2] ] ) ? '1' : null;
|
|
}
|
|
if ( preg_match( '/SELECT meta_value.*post_id = (\d+).*meta_key = \'([^\']+)\'/', $flat, $m ) ) {
|
|
$entry = $store[ $m[1] ][ $m[2] ] ?? null;
|
|
if ( ! $entry ) {
|
|
return null;
|
|
}
|
|
if ( $entry['expires_at'] && $entry['expires_at'] < time() ) {
|
|
return null;
|
|
}
|
|
return $entry['value'];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public function get_results( string $sql, $output = null ): array {
|
|
$store = ZoneWarmTest::$store;
|
|
$result = [];
|
|
foreach ( $store as $post_id => $keys ) {
|
|
foreach ( $keys as $meta_key => $entry ) {
|
|
if ( $entry['expires_at'] && $entry['expires_at'] < time() ) {
|
|
continue;
|
|
}
|
|
$result[] = (object) [ 'meta_key' => $meta_key, 'meta_value' => $entry['value'] ];
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function insert( string $table, array $data, $format = null ): int|false {
|
|
ZoneWarmTest::$store[ $data['post_id'] ][ $data['meta_key'] ] = [
|
|
'value' => $data['meta_value'],
|
|
'expires_at' => isset( $data['expires_at'] ) ? strtotime( $data['expires_at'] ) : null,
|
|
];
|
|
return 1;
|
|
}
|
|
|
|
public function update( string $table, array $data, array $where, $format = null, $where_format = null ): int|false {
|
|
// For simplicity, find by scanning store.
|
|
foreach ( ZoneWarmTest::$store as $post_id => &$keys ) {
|
|
foreach ( $keys as $meta_key => &$entry ) {
|
|
if ( isset( $data['meta_value'] ) ) {
|
|
$entry['value'] = $data['meta_value'];
|
|
}
|
|
if ( isset( $data['expires_at'] ) ) {
|
|
$entry['expires_at'] = strtotime( $data['expires_at'] );
|
|
}
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
public function delete( string $table, array $where, $format = null ): int|false {
|
|
$post_id = $where['post_id'] ?? null;
|
|
$meta_key = $where['meta_key'] ?? null;
|
|
|
|
if ( $post_id && $meta_key ) {
|
|
unset( ZoneWarmTest::$store[ $post_id ][ $meta_key ] );
|
|
} elseif ( $post_id ) {
|
|
unset( ZoneWarmTest::$store[ $post_id ] );
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
public function query( string $sql ): int|bool {
|
|
$store = &ZoneWarmTest::$store;
|
|
$flat = preg_replace( '/\s+/', ' ', $sql );
|
|
|
|
// INSERT ... ON DUPLICATE KEY UPDATE (from set() via TMDO_DB::upsert())
|
|
// Pattern: VALUES (post_id, 'meta_key', 'value', expires_or_NULL, 'now') ON DUPLICATE KEY
|
|
if ( stripos( $flat, 'ON DUPLICATE KEY' ) !== false && stripos( $flat, 'COALESCE' ) === false ) {
|
|
if ( preg_match( "/VALUES \((\d+), '([^']+)', '([^']*)', (NULL|'[^']+'), '[^']+'\)/i", $flat, $m ) ) {
|
|
$post_id = (int) $m[1];
|
|
$meta_key = $m[2];
|
|
$meta_value = $m[3];
|
|
$expires_at = 'NULL' === $m[4] ? null : strtotime( trim( $m[4], "'" ) );
|
|
$store[ $post_id ][ $meta_key ] = array(
|
|
'value' => $meta_value,
|
|
'expires_at' => $expires_at,
|
|
);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// INSERT ... ON DUPLICATE KEY UPDATE meta_value = CAST(COALESCE...) + N (from increment())
|
|
if ( stripos( $flat, 'COALESCE' ) !== false ) {
|
|
if ( preg_match( "/VALUES \((\d+), '([^']+)', (\d+),/i", $flat, $m ) ) {
|
|
$post_id = (int) $m[1];
|
|
$meta_key = $m[2];
|
|
$by = (int) $m[3];
|
|
$current = (int) ( $store[ $post_id ][ $meta_key ]['value'] ?? 0 );
|
|
$store[ $post_id ][ $meta_key ] = array(
|
|
'value' => (string) ( $current + $by ),
|
|
'expires_at' => null,
|
|
);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
};
|
|
}
|
|
|
|
// ── set / get ────────────────────────────────────────────────────────────
|
|
|
|
public function test_set_and_get_basic_value(): void {
|
|
WPDO_Zone_Warm::set( 1, 'hp_views', '42' );
|
|
$this->assertSame( '42', WPDO_Zone_Warm::get( 1, 'hp_views' ) );
|
|
}
|
|
|
|
public function test_get_returns_null_for_missing_key(): void {
|
|
$this->assertNull( WPDO_Zone_Warm::get( 99, 'missing_key' ) );
|
|
}
|
|
|
|
public function test_set_overwrites_existing_value(): void {
|
|
WPDO_Zone_Warm::set( 1, 'hp_views', '10' );
|
|
WPDO_Zone_Warm::set( 1, 'hp_views', '20' );
|
|
// The store update mock replaces all entries for simplicity.
|
|
$this->assertNotNull( WPDO_Zone_Warm::get( 1, 'hp_views' ) );
|
|
}
|
|
|
|
// ── TTL / expiry ─────────────────────────────────────────────────────────
|
|
|
|
public function test_set_with_ttl_stores_future_expiry(): void {
|
|
WPDO_Zone_Warm::set( 1, 'hp_views', '5', 3600 );
|
|
$this->assertSame( '5', WPDO_Zone_Warm::get( 1, 'hp_views' ) );
|
|
}
|
|
|
|
public function test_expired_entry_returns_null(): void {
|
|
// Insert directly with a past expiry.
|
|
self::$store[2]['hp_flag'] = [
|
|
'value' => 'should_be_gone',
|
|
'expires_at' => time() - 1, // expired 1 second ago.
|
|
];
|
|
$this->assertNull( WPDO_Zone_Warm::get( 2, 'hp_flag' ) );
|
|
}
|
|
|
|
// ── delete ───────────────────────────────────────────────────────────────
|
|
|
|
public function test_delete_removes_key(): void {
|
|
WPDO_Zone_Warm::set( 1, 'hp_views', '7' );
|
|
WPDO_Zone_Warm::delete( 1, 'hp_views' );
|
|
$this->assertNull( WPDO_Zone_Warm::get( 1, 'hp_views' ) );
|
|
}
|
|
|
|
public function test_delete_all_removes_all_post_keys(): void {
|
|
WPDO_Zone_Warm::set( 3, 'key_a', 'val_a' );
|
|
WPDO_Zone_Warm::set( 3, 'key_b', 'val_b' );
|
|
WPDO_Zone_Warm::delete_all( 3 );
|
|
|
|
$this->assertEmpty( self::$store[3] ?? [] );
|
|
}
|
|
|
|
// ── Additional tests ─────────────────────────────────────────────────────
|
|
|
|
public function test_table_returns_warm_table_name(): void {
|
|
$this->assertSame( 'wp_wpdo_warm', WPDO_Zone_Warm::table() );
|
|
}
|
|
|
|
public function test_delete_specific_key_leaves_other_keys_intact(): void {
|
|
WPDO_Zone_Warm::set( 4, 'key_keep', 'val_keep' );
|
|
WPDO_Zone_Warm::set( 4, 'key_drop', 'val_drop' );
|
|
WPDO_Zone_Warm::delete( 4, 'key_drop' );
|
|
|
|
$this->assertNull( WPDO_Zone_Warm::get( 4, 'key_drop' ) );
|
|
// key_keep should still be readable.
|
|
$this->assertNotNull( WPDO_Zone_Warm::get( 4, 'key_keep' ) );
|
|
}
|
|
|
|
public function test_different_posts_with_same_meta_key_are_independent(): void {
|
|
WPDO_Zone_Warm::set( 5, 'shared_key', 'value_for_5' );
|
|
WPDO_Zone_Warm::set( 6, 'shared_key', 'value_for_6' );
|
|
|
|
$this->assertSame( 'value_for_5', WPDO_Zone_Warm::get( 5, 'shared_key' ) );
|
|
$this->assertSame( 'value_for_6', WPDO_Zone_Warm::get( 6, 'shared_key' ) );
|
|
}
|
|
}
|