query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' ); $wpdb->query( 'CREATE TABLE `' . self::POSTS . '` ( ID bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_title text NOT NULL, post_type varchar(20) NOT NULL DEFAULT \'post\', post_status varchar(20) NOT NULL DEFAULT \'publish\', post_date datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, post_date_gmt datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, post_modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, post_modified_gmt datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, post_author bigint(20) unsigned NOT NULL DEFAULT 0, post_content longtext NOT NULL, post_excerpt text NOT NULL, comment_status varchar(20) NOT NULL DEFAULT \'open\', ping_status varchar(20) NOT NULL DEFAULT \'open\', post_password varchar(255) NOT NULL DEFAULT \'\', post_name varchar(200) NOT NULL DEFAULT \'\', to_ping text NOT NULL, pinged text NOT NULL, post_content_filtered longtext NOT NULL, post_parent bigint(20) unsigned NOT NULL DEFAULT 0, guid varchar(255) NOT NULL DEFAULT \'\', menu_order int(11) NOT NULL DEFAULT 0, post_mime_type varchar(100) NOT NULL DEFAULT \'\', comment_count bigint(20) NOT NULL DEFAULT 0, PRIMARY KEY (ID), KEY post_type (post_type), KEY post_title (post_title(64)) ) DEFAULT CHARACTER SET utf8mb4' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' ); $wpdb->query( 'CREATE TABLE `' . self::POSTMETA . '` ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, post_id bigint(20) unsigned NOT NULL DEFAULT 0, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY post_id (post_id), KEY meta_key (meta_key(191)) ) DEFAULT CHARACTER SET utf8mb4' ); } public static function tearDownAfterClass(): void { global $wpdb; $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTS . '`' ); $wpdb->query( 'DROP TABLE IF EXISTS `' . self::POSTMETA . '`' ); } protected function setUp(): void { global $wpdb; $wpdb->query( 'TRUNCATE TABLE `' . self::POSTS . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' ); } // ── create() ────────────────────────────────────────────────────────────── public function test_create_inserts_requested_count_of_products(): void { $result = WPDO_Post_Stress_Tester::create( 'product', 5 ); $this->assertSame( 5, $result['created'] ); global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTS . "` WHERE post_type = 'product'" ); $this->assertSame( 5, $count ); } public function test_create_uses_stress_test_prefix_in_post_title(): void { WPDO_Post_Stress_Tester::create( 'product', 3 ); global $wpdb; $prefix_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `" . self::POSTS . "` WHERE post_title LIKE %s", WPDO_Post_Stress_Tester::TEST_POST_PREFIX . '%' ) ); $this->assertSame( 3, $prefix_count ); } public function test_create_seeds_postmeta_for_each_post(): void { WPDO_Post_Stress_Tester::create( 'product', 2 ); global $wpdb; // Each test product should have at least the 5 critical wc_product keys. $meta_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "`" ); $this->assertGreaterThanOrEqual( 10, $meta_count, 'At least 5 keys × 2 posts = 10 rows.' ); // Verify _price was set on every test product. $price_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "` WHERE meta_key = '_price'" ); $this->assertSame( 2, $price_count ); } public function test_create_supports_hp_listing_post_type(): void { $result = WPDO_Post_Stress_Tester::create( 'hp_listing', 4 ); $this->assertSame( 4, $result['created'] ); global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTS . "` WHERE post_type = 'hp_listing'" ); $this->assertSame( 4, $count ); // hp_listing seeds hp_price, not _price. $hp_price_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "` WHERE meta_key = 'hp_price'" ); $this->assertSame( 4, $hp_price_count ); } public function test_create_rejects_unsupported_post_type(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Stress_Tester::create( 'bogus_type', 3 ); } public function test_create_rejects_zero_count(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Stress_Tester::create( 'product', 0 ); } public function test_create_rejects_excessive_count(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Stress_Tester::create( 'product', 100001 ); } // ── count_test_posts() ──────────────────────────────────────────────────── public function test_count_test_posts_returns_zero_for_empty(): void { $this->assertSame( 0, WPDO_Post_Stress_Tester::count_test_posts() ); } public function test_count_test_posts_counts_only_stress_prefix(): void { // Seed 2 stress posts + 1 real post. WPDO_Post_Stress_Tester::create( 'product', 2 ); global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => 9999, 'post_title' => 'Real product not from stress', 'post_type' => 'product', 'post_content' => '', 'post_excerpt' => '', 'post_content_filtered' => '', 'to_ping' => '', 'pinged' => '', ) ); $this->assertSame( 2, WPDO_Post_Stress_Tester::count_test_posts() ); } // ── cleanup() ───────────────────────────────────────────────────────────── public function test_cleanup_removes_all_stress_posts_and_their_meta(): void { WPDO_Post_Stress_Tester::create( 'product', 5 ); // Verify pre-state. $this->assertSame( 5, WPDO_Post_Stress_Tester::count_test_posts() ); $result = WPDO_Post_Stress_Tester::cleanup(); $this->assertSame( 5, $result['deleted_posts'] ); global $wpdb; $post_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTS . "`" ); $meta_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "`" ); $this->assertSame( 0, $post_count ); $this->assertSame( 0, $meta_count, 'cleanup() must cascade delete postmeta.' ); } public function test_cleanup_preserves_non_stress_posts(): void { // Real post with prefix-collision-immune title. global $wpdb; $wpdb->insert( self::POSTS, array( 'ID' => 9999, 'post_title' => 'Real product not from stress', 'post_type' => 'product', 'post_content' => '', 'post_excerpt' => '', 'post_content_filtered' => '', 'to_ping' => '', 'pinged' => '', ) ); $wpdb->insert( self::POSTMETA, array( 'post_id' => 9999, 'meta_key' => '_price', 'meta_value' => '50.00', ) ); WPDO_Post_Stress_Tester::create( 'product', 3 ); $result = WPDO_Post_Stress_Tester::cleanup(); $this->assertSame( 3, $result['deleted_posts'] ); // Real post + its meta survive. $remaining_posts = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTS . "`" ); $remaining_meta = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "`" ); $this->assertSame( 1, $remaining_posts ); $this->assertSame( 1, $remaining_meta ); } public function test_cleanup_idempotent_on_empty_state(): void { $result1 = WPDO_Post_Stress_Tester::cleanup(); $result2 = WPDO_Post_Stress_Tester::cleanup(); $this->assertSame( 0, $result1['deleted_posts'] ); $this->assertSame( 0, $result2['deleted_posts'] ); } // ── v2.11.2: create_realistic() — uses wp_insert_post + update_post_meta ─ public function test_create_realistic_uses_wp_insert_post(): void { $result = WPDO_Post_Stress_Tester::create_realistic( 'product', 3 ); $this->assertSame( 3, $result['created'] ); $this->assertSame( 'realistic', $result['mode'] ?? '' ); global $wpdb; $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTS . "` WHERE post_type = 'product'" ); $this->assertSame( 3, $count ); } public function test_create_realistic_writes_postmeta_via_update_post_meta(): void { $result = WPDO_Post_Stress_Tester::create_realistic( 'product', 2 ); $this->assertSame( 2, $result['created'] ); global $wpdb; $price_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "` WHERE meta_key = '_price'" ); $this->assertSame( 2, $price_count, 'Each realistic product seeds _price.' ); } public function test_create_realistic_uses_stress_test_prefix(): void { WPDO_Post_Stress_Tester::create_realistic( 'product', 2 ); global $wpdb; $prefixed = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `" . self::POSTS . "` WHERE post_title LIKE %s", WPDO_Post_Stress_Tester::TEST_POST_PREFIX . '%' ) ); $this->assertSame( 2, $prefixed ); } public function test_create_realistic_supports_hp_listing(): void { $result = WPDO_Post_Stress_Tester::create_realistic( 'hp_listing', 4 ); $this->assertSame( 4, $result['created'] ); global $wpdb; $hp_price_count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `" . self::POSTMETA . "` WHERE meta_key = 'hp_price'" ); $this->assertSame( 4, $hp_price_count ); } public function test_create_realistic_rejects_unsupported_post_type(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Stress_Tester::create_realistic( 'bogus_type', 2 ); } public function test_create_realistic_rejects_invalid_count(): void { $this->expectException( InvalidArgumentException::class ); WPDO_Post_Stress_Tester::create_realistic( 'product', 0 ); } public function test_cleanup_removes_realistic_created_posts(): void { WPDO_Post_Stress_Tester::create_realistic( 'product', 3 ); $this->assertSame( 3, WPDO_Post_Stress_Tester::count_test_posts() ); $cleanup = WPDO_Post_Stress_Tester::cleanup(); $this->assertSame( 3, $cleanup['deleted_posts'] ); $this->assertSame( 0, WPDO_Post_Stress_Tester::count_test_posts() ); } // ── v2.11.4: state machine (start / cancel / get_state / get_progress / run_batch) ─ protected function tearDown(): void { // Reset persisted state between state-machine tests so each test starts idle. unset( $GLOBALS['_wp_options'][ WPDO_Post_Stress_Tester::OPT_STATE ] ); unset( $GLOBALS['_wp_transients'][ WPDO_Post_Stress_Tester::CANCEL_FLAG ] ); unset( $GLOBALS['_wp_transients']['wpdo_post_stress_pump_lock'] ); } public function test_get_state_returns_empty_when_idle(): void { $this->assertSame( array(), WPDO_Post_Stress_Tester::get_state() ); } public function test_get_progress_returns_idle_when_no_state(): void { $progress = WPDO_Post_Stress_Tester::get_progress( false ); $this->assertSame( 'idle', $progress['status'] ); $this->assertSame( 0, $progress['processed'] ); } public function test_start_persists_state_with_running_status(): void { $result = WPDO_Post_Stress_Tester::start( 'product', 10, 'fast', 5 ); $this->assertTrue( $result['ok'] ); $state = $result['state']; $this->assertSame( 'running', $state['status'] ); $this->assertSame( 'product', $state['post_type'] ); $this->assertSame( 'fast', $state['mode'] ); $this->assertSame( 10, $state['target'] ); $this->assertSame( 5, $state['batch_size'] ); $this->assertSame( 0, $state['processed'] ); } public function test_start_rejects_unsupported_post_type(): void { $result = WPDO_Post_Stress_Tester::start( 'bogus_type', 10 ); $this->assertFalse( $result['ok'] ); $this->assertSame( 'unsupported_post_type', $result['error'] ); } public function test_start_rejects_zero_target(): void { $result = WPDO_Post_Stress_Tester::start( 'product', 0 ); $this->assertFalse( $result['ok'] ); } public function test_start_rejects_excessive_target(): void { $result = WPDO_Post_Stress_Tester::start( 'product', 100001 ); $this->assertFalse( $result['ok'] ); } public function test_start_rejects_invalid_mode(): void { $result = WPDO_Post_Stress_Tester::start( 'product', 10, 'turbo' ); $this->assertFalse( $result['ok'] ); $this->assertSame( 'invalid mode', $result['error'] ); } public function test_start_rejects_concurrent_run(): void { WPDO_Post_Stress_Tester::start( 'product', 10 ); $result = WPDO_Post_Stress_Tester::start( 'product', 5 ); $this->assertFalse( $result['ok'] ); $this->assertSame( 'already_running', $result['error'] ); } public function test_start_clamps_batch_size_above_max(): void { $result = WPDO_Post_Stress_Tester::start( 'product', 10, 'fast', 5000 ); $this->assertTrue( $result['ok'] ); $this->assertSame( WPDO_Post_Stress_Tester::MAX_BATCH_SIZE, $result['state']['batch_size'] ); } public function test_run_batch_advances_processed_count(): void { WPDO_Post_Stress_Tester::start( 'product', 6, 'fast', 3 ); WPDO_Post_Stress_Tester::run_batch(); $progress = WPDO_Post_Stress_Tester::get_progress( false ); $this->assertSame( 3, $progress['processed'] ); $this->assertSame( 1, $progress['batches_done'] ); $this->assertSame( 'running', $progress['status'] ); WPDO_Post_Stress_Tester::run_batch(); $progress = WPDO_Post_Stress_Tester::get_progress( false ); $this->assertSame( 6, $progress['processed'] ); $this->assertSame( 'completed', $progress['status'] ); } public function test_run_batch_creates_actual_posts(): void { WPDO_Post_Stress_Tester::start( 'product', 4, 'fast', 4 ); WPDO_Post_Stress_Tester::run_batch(); $this->assertSame( 4, WPDO_Post_Stress_Tester::count_test_posts() ); } public function test_run_batch_finalizes_with_benchmark(): void { WPDO_Post_Stress_Tester::start( 'product', 2, 'fast', 2 ); WPDO_Post_Stress_Tester::run_batch(); $state = WPDO_Post_Stress_Tester::get_state(); $this->assertSame( 'completed', $state['status'] ); $this->assertIsArray( $state['benchmark'] ); $this->assertArrayHasKey( 'write', $state['benchmark'] ); $this->assertArrayHasKey( 'db_sizes', $state['benchmark'] ); $this->assertSame( 'product', $state['benchmark']['post_type'] ); } public function test_cancel_marks_state_as_cancelled(): void { WPDO_Post_Stress_Tester::start( 'product', 100, 'fast', 50 ); $result = WPDO_Post_Stress_Tester::cancel(); $this->assertTrue( $result['ok'] ); $this->assertSame( 'cancelled', $result['state']['status'] ); // In-flight batch run after cancel must NOT bump status back to running. WPDO_Post_Stress_Tester::run_batch(); $state = WPDO_Post_Stress_Tester::get_state(); $this->assertSame( 'cancelled', $state['status'] ); } public function test_cancel_returns_no_active_job_when_idle(): void { $result = WPDO_Post_Stress_Tester::cancel(); $this->assertTrue( $result['ok'] ); $this->assertSame( 'no_active_job', $result['message'] ?? '' ); } public function test_get_progress_includes_pct_and_eta(): void { WPDO_Post_Stress_Tester::start( 'product', 10, 'fast', 5 ); WPDO_Post_Stress_Tester::run_batch(); $progress = WPDO_Post_Stress_Tester::get_progress( false ); $this->assertArrayHasKey( 'pct', $progress ); $this->assertArrayHasKey( 'rate_per_sec', $progress ); $this->assertArrayHasKey( 'elapsed_sec', $progress ); $this->assertArrayHasKey( 'eta_sec', $progress ); $this->assertArrayHasKey( 'test_post_count', $progress ); $this->assertSame( 50.0, $progress['pct'] ); // 5/10 = 50% } public function test_run_benchmark_returns_post_type_aware_query_probes(): void { WPDO_Post_Stress_Tester::start( 'hp_listing', 4, 'fast', 4 ); WPDO_Post_Stress_Tester::run_batch(); $state = WPDO_Post_Stress_Tester::get_state(); $bench = $state['benchmark']; $this->assertSame( 'hp_listing', $bench['post_type'] ); // query is post_type-aware; in production the flat table exists so this // returns 3 probes (point/range/eav_baseline). In integration tests the // flat table doesn't exist (different prefix), so we only verify the // structure is present and post_type-aware. $this->assertIsArray( $bench['query'] ); } public function test_run_batch_realistic_uses_wp_insert_post(): void { WPDO_Post_Stress_Tester::start( 'product', 3, 'realistic', 3 ); WPDO_Post_Stress_Tester::run_batch(); $state = WPDO_Post_Stress_Tester::get_state(); $this->assertSame( 'completed', $state['status'] ); $this->assertSame( 3, $state['processed'] ); $this->assertSame( 'realistic', $state['mode'] ); } }