meta_key => [value, expires_at] */ public static array $store = []; /** Flushed view counts captured during flush_views_to_postmeta(). */ public static array $flushed = []; protected function setUp(): void { self::$store = []; self::$flushed = []; $GLOBALS['_wp_postmeta'] = []; $this->setupWpdbMock(); } private function setupWpdbMock(): void { global $wpdb; $wpdb = new class { public string $prefix = 'wp_'; public string $postmeta = 'wp_postmeta'; public string $posts = 'wp_posts'; 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 = &ListingStatsTest::$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 { $flat = preg_replace( '/\s+/', ' ', $sql ); // Postmeta batch query (e.g. hp_view_count) → read from $GLOBALS['_wp_postmeta']. if ( strpos( $flat, 'wp_postmeta' ) !== false ) { $result = []; foreach ( $GLOBALS['_wp_postmeta'] ?? [] as $post_id => $meta ) { if ( isset( $meta['hp_view_count'] ) ) { $row = [ 'post_id' => (string) $post_id, 'meta_value' => (string) $meta['hp_view_count'] ]; $result[] = $output === ARRAY_A ? $row : (object) $row; } } return $result; } // Warm zone query → return from store, filtered by meta_key in SQL. $store = ListingStatsTest::$store; $result = []; $filter_key = null; if ( preg_match( "/meta_key = '([^']+)'/", $flat, $m ) ) { $filter_key = $m[1]; } foreach ( $store as $post_id => $keys ) { foreach ( $keys as $meta_key => $entry ) { if ( $filter_key && $meta_key !== $filter_key ) { continue; } if ( $entry['expires_at'] && $entry['expires_at'] < time() ) { continue; } $row = [ 'post_id' => $post_id, 'meta_key' => $meta_key, 'meta_value' => $entry['value'] ]; $result[] = $output === ARRAY_A ? $row : (object) $row; } } return $result; } public function insert( string $table, array $data, $format = null ): int|false { ListingStatsTest::$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, $f1 = null, $f2 = null ): int|false { // Scan store and update matching entries (where clause uses id=1 placeholder). foreach ( ListingStatsTest::$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( ListingStatsTest::$store[ $post_id ][ $meta_key ] ); } elseif ( $post_id ) { unset( ListingStatsTest::$store[ $post_id ] ); } return 1; } public function query( string $sql ): int|bool { $store = &ListingStatsTest::$store; $flat = preg_replace( '/\s+/', ' ', $sql ); // Atomic postmeta increment — from flush_views_to_postmeta (P1-22). // SQL: UPDATE `wp_postmeta` SET meta_value = meta_value + N WHERE post_id = P AND meta_key = 'hp_view_count' if ( stripos( $flat, 'meta_value = meta_value +' ) !== false ) { if ( preg_match( '/meta_value = meta_value \+ (\d+).*post_id = (\d+)/i', $flat, $m ) ) { $by = (int) $m[1]; $post_id = (int) $m[2]; if ( isset( $GLOBALS['_wp_postmeta'][ $post_id ]['hp_view_count'] ) ) { $GLOBALS['_wp_postmeta'][ $post_id ]['hp_view_count'] += $by; return 1; } } return 0; // no existing row — caller will add_post_meta } // INSERT ... ON DUPLICATE KEY UPDATE (from set() via TMDO_DB::upsert()) 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 = $m[4] === 'NULL' ? null : strtotime( trim( $m[4], "'" ) ); $store[ $post_id ][ $meta_key ] = [ '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 ] = [ 'value' => (string) ( $current + $by ), 'expires_at' => null, ]; return 1; } } return 0; } }; } // ── increment_view ─────────────────────────────────────────────────────── public function test_increment_view_starts_at_one(): void { TMDO_Listing_Stats::increment_view( 10 ); $this->assertSame( '1', TMDO_Zone_Warm::get( 10, TMDO_Listing_Stats::VIEW_KEY ) ); } public function test_increment_view_accumulates(): void { TMDO_Listing_Stats::increment_view( 10 ); TMDO_Listing_Stats::increment_view( 10 ); TMDO_Listing_Stats::increment_view( 10 ); $this->assertSame( '3', TMDO_Zone_Warm::get( 10, TMDO_Listing_Stats::VIEW_KEY ) ); } // ── get_view_count ─────────────────────────────────────────────────────── public function test_get_view_count_reads_warm_zone_first(): void { TMDO_Listing_Stats::increment_view( 20 ); TMDO_Listing_Stats::increment_view( 20 ); // postmeta has 0 (default), warm has 2. $this->assertSame( 2, TMDO_Listing_Stats::get_view_count( 20 ) ); } public function test_get_view_count_falls_back_to_postmeta(): void { // No warm entry → falls back to postmeta. $GLOBALS['_wp_postmeta'][30]['hp_view_count'] = 99; $this->assertSame( 99, TMDO_Listing_Stats::get_view_count( 30 ) ); } public function test_get_view_count_returns_zero_when_empty(): void { $this->assertSame( 0, TMDO_Listing_Stats::get_view_count( 999 ) ); } // ── flush_views_to_postmeta ────────────────────────────────────────────── public function test_flush_moves_count_to_postmeta(): void { TMDO_Listing_Stats::increment_view( 40 ); TMDO_Listing_Stats::increment_view( 40 ); $flushed = TMDO_Listing_Stats::flush_views_to_postmeta(); $this->assertSame( 1, $flushed ); $this->assertSame( 2, (int) ( $GLOBALS['_wp_postmeta'][40]['hp_view_count'] ?? 0 ) ); } public function test_flush_clears_warm_entry(): void { TMDO_Listing_Stats::increment_view( 50 ); TMDO_Listing_Stats::flush_views_to_postmeta(); $this->assertNull( TMDO_Zone_Warm::get( 50, TMDO_Listing_Stats::VIEW_KEY ) ); } public function test_flush_returns_zero_when_no_warm_entries(): void { $this->assertSame( 0, TMDO_Listing_Stats::flush_views_to_postmeta() ); } public function test_flush_accumulates_on_top_of_existing_postmeta(): void { $GLOBALS['_wp_postmeta'][60]['hp_view_count'] = 10; TMDO_Listing_Stats::increment_view( 60 ); TMDO_Listing_Stats::increment_view( 60 ); TMDO_Listing_Stats::flush_views_to_postmeta(); $this->assertSame( 12, (int) ( $GLOBALS['_wp_postmeta'][60]['hp_view_count'] ?? 0 ) ); } }