d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
161 lines
6.0 KiB
PHP
161 lines
6.0 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
if ( ! function_exists( '__' ) ) {
|
||
function __( string $text, string $domain = 'default' ): string { return $text; }
|
||
}
|
||
if ( ! function_exists( 'number_format_i18n' ) ) {
|
||
function number_format_i18n( $n, int $decimals = 0 ): string {
|
||
return number_format( (float) $n, $decimals );
|
||
}
|
||
}
|
||
|
||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-logger.php';
|
||
require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-feature-flags.php';
|
||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-module-rules.php';
|
||
require_once dirname( __DIR__, 3 ) . '/includes/advisor/class-tmdo-module-detector.php';
|
||
|
||
/**
|
||
* Unit tests for WPDO_Module_Rules + WPDO_Module_Detector (v2.5.0 M16).
|
||
*
|
||
* Pure-logic tests using mocked WPDO_Compatibility (via dynamic class
|
||
* substitution where needed) and option-driven post counts.
|
||
*/
|
||
class ModuleDetectorTest extends TestCase {
|
||
|
||
protected function setUp(): void {
|
||
$GLOBALS['_wp_options'] = array();
|
||
// Reset Feature_Flags cache.
|
||
$ref = new ReflectionClass( WPDO_Feature_Flags::class );
|
||
$prop = $ref->getProperty( 'cache' );
|
||
$prop->setAccessible( true );
|
||
$prop->setValue( null, null );
|
||
$this->setup_wpdb_mock();
|
||
}
|
||
|
||
private function setup_wpdb_mock(): void {
|
||
global $wpdb;
|
||
$wpdb = new class {
|
||
public string $prefix = 'wp_';
|
||
public string $posts = 'wp_posts';
|
||
public string $postmeta = 'wp_postmeta';
|
||
public string $options = 'wp_options';
|
||
public array $count_overrides = array();
|
||
public function prepare( string $sql, ...$args ): string {
|
||
$i = 0;
|
||
return preg_replace_callback( '/%[sd]/', function() use ( &$i, $args ) {
|
||
return is_string( $args[ $i ] ?? null ) ? "'" . $args[ $i++ ] . "'" : (string) ( $args[ $i++ ] ?? '?' );
|
||
}, $sql );
|
||
}
|
||
public function get_var( string $sql ) {
|
||
// Mock by inspecting SQL pattern.
|
||
if ( str_contains( $sql, "post_type =" ) ) {
|
||
if ( preg_match( "/post_type = '([^']+)'/", $sql, $m ) ) {
|
||
return (string) ( $this->count_overrides[ $m[1] ] ?? 0 );
|
||
}
|
||
}
|
||
if ( str_contains( $sql, "post_status = 'trash'" ) ) {
|
||
return (string) ( $this->count_overrides['__trash__'] ?? 0 );
|
||
}
|
||
return '0';
|
||
}
|
||
public function get_results( string $sql, $output = ARRAY_A ): array { return array(); }
|
||
};
|
||
}
|
||
|
||
public function test_module_rules_known_modules_count(): void {
|
||
$modules = WPDO_Module_Rules::known_modules();
|
||
$this->assertGreaterThanOrEqual( 14, count( $modules ), '預期 ≥ 14 個 module(9 HPCT + 6 zone - 1 dup)' );
|
||
$this->assertContains( 'reviews', $modules );
|
||
$this->assertContains( 'warm', $modules );
|
||
$this->assertContains( 'archive', $modules );
|
||
$this->assertContains( 'hot_hp_listing', $modules );
|
||
}
|
||
|
||
public function test_module_rule_for_module_returns_array(): void {
|
||
$rule = WPDO_Module_Rules::for_module( 'reviews' );
|
||
$this->assertIsArray( $rule );
|
||
$this->assertArrayHasKey( 'compat_required', $rule );
|
||
$this->assertArrayHasKey( 'description', $rule );
|
||
}
|
||
|
||
public function test_module_rule_unknown_module_returns_null(): void {
|
||
$this->assertNull( WPDO_Module_Rules::for_module( 'totally_fake_module_xyz' ) );
|
||
}
|
||
|
||
public function test_detector_skip_when_module_not_idle(): void {
|
||
// Set reviews to dual_write.
|
||
WPDO_Feature_Flags::set( 'reviews', 'dual_write' );
|
||
$r = WPDO_Module_Detector::detect_one( 'reviews' );
|
||
$this->assertSame( 'skip', $r['recommendation'] );
|
||
$this->assertFalse( $r['available'] );
|
||
$this->assertNotEmpty( $r['blockers'] );
|
||
$this->assertSame( 'dual_write', $r['current_state'] );
|
||
}
|
||
|
||
public function test_detector_warm_module_recommends_for_any_site(): void {
|
||
// warm module's compat_required is empty + no post_type.
|
||
$r = WPDO_Module_Detector::detect_one( 'warm' );
|
||
$this->assertTrue( $r['available'] );
|
||
$this->assertSame( 'enable', $r['recommendation'] );
|
||
$this->assertGreaterThan( 0, $r['confidence'] );
|
||
$this->assertNotEmpty( $r['suggested_action'] );
|
||
$this->assertSame( 'dual_write', $r['suggested_action']['to_state'] );
|
||
}
|
||
|
||
public function test_detector_archive_blocked_when_no_trashed_posts(): void {
|
||
// trashed = 0, threshold = 50.
|
||
global $wpdb;
|
||
$wpdb->count_overrides['__trash__'] = 0;
|
||
$r = WPDO_Module_Detector::detect_one( 'archive' );
|
||
$this->assertSame( 'wait', $r['recommendation'] );
|
||
$this->assertFalse( $r['available'] );
|
||
}
|
||
|
||
public function test_detector_archive_passes_when_enough_trashed(): void {
|
||
global $wpdb;
|
||
$wpdb->count_overrides['__trash__'] = 100;
|
||
$r = WPDO_Module_Detector::detect_one( 'archive' );
|
||
$this->assertSame( 'enable', $r['recommendation'] );
|
||
$this->assertTrue( $r['available'] );
|
||
}
|
||
|
||
public function test_detector_hivepress_required_blocks_when_inactive(): void {
|
||
// reviews requires hivepress; Compatibility class isn't loaded in this test.
|
||
// Without Compatibility class, the helper returns the full required list as missing.
|
||
$r = WPDO_Module_Detector::detect_one( 'reviews' );
|
||
$this->assertSame( 'skip', $r['recommendation'] );
|
||
$this->assertNotEmpty( $r['blockers'] );
|
||
}
|
||
|
||
public function test_get_actionable_filters_by_confidence(): void {
|
||
$actionable = WPDO_Module_Detector::get_actionable( 0.5 );
|
||
$this->assertIsArray( $actionable );
|
||
// Each result should have available=true and recommendation=enable.
|
||
foreach ( $actionable as $module => $r ) {
|
||
$this->assertTrue( $r['available'], "$module should be available" );
|
||
$this->assertSame( 'enable', $r['recommendation'] );
|
||
$this->assertGreaterThanOrEqual( 0.5, $r['confidence'] );
|
||
}
|
||
}
|
||
|
||
public function test_get_actionable_sorts_by_confidence_desc(): void {
|
||
$actionable = WPDO_Module_Detector::get_actionable( 0.0 );
|
||
$last_confidence = 1.0;
|
||
foreach ( $actionable as $r ) {
|
||
$this->assertLessThanOrEqual( $last_confidence, (float) $r['confidence'] );
|
||
$last_confidence = (float) $r['confidence'];
|
||
}
|
||
}
|
||
|
||
public function test_detect_all_returns_entry_for_each_known_module(): void {
|
||
$all = WPDO_Module_Detector::detect_all( true );
|
||
$known = WPDO_Module_Rules::known_modules();
|
||
foreach ( $known as $m ) {
|
||
$this->assertArrayHasKey( $m, $all, "Detector should return entry for $m" );
|
||
}
|
||
}
|
||
}
|