test: 移植 ListingStatsTest(9 tests)
unit bootstrap 改載入真實 TMDO_Listing_Stats:先 define TMDO_TEST_SKIP_LISTING_STATS_STUB,核心 bootstrap 便會跳過它的 stub。 145 → 154 tests。
This commit is contained in:
+4
-3
@@ -13,6 +13,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// Load the real TMDO_Listing_Stats below instead of core's stub.
|
||||
define( 'TMDO_TEST_SKIP_LISTING_STATS_STUB', true );
|
||||
|
||||
$tmdo_core_bootstrap = dirname( __DIR__, 2 ) . '/2meet-data-optimizer/tests/bootstrap.php';
|
||||
if ( ! file_exists( $tmdo_core_bootstrap ) ) {
|
||||
throw new RuntimeException(
|
||||
@@ -44,9 +47,7 @@ $tmdo_hp_files = array(
|
||||
'includes/class-tmdo-hivepress-transient-filter.php',
|
||||
'includes/class-tmdo-hivepress-term-comment-fields.php',
|
||||
'includes/class-tmdo-hpct-import.php',
|
||||
// class-tmdo-listing-stats.php is deliberately NOT loaded: the core unit
|
||||
// bootstrap already declares a TMDO_Listing_Stats stub, and no test in this
|
||||
// suite exercises the real implementation.
|
||||
'includes/class-tmdo-listing-stats.php',
|
||||
'admin/class-tmdo-admin-hivepress.php',
|
||||
'cli/class-tmdo-cli-hivepress.php',
|
||||
);
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for TMDO_Listing_Stats — Zone B/D integration.
|
||||
*
|
||||
* Tests view counter logic and flush-to-postmeta using
|
||||
* the same in-memory wpdb mock as ZoneWarmTest.
|
||||
*/
|
||||
class ListingStatsTest extends TestCase {
|
||||
|
||||
/** In-memory warm store: post_id => 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 ) );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user