From f72a315986956a9b6feff5d55070a0d333dd38c1 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sat, 26 Sep 2026 18:21:31 +0800 Subject: [PATCH 01/16] test(2meet-data-optimizer): add live test runner and failing shadow_read write suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add tests/live: a `wp eval-file` runner (PASS/FAIL/XFAIL/XPASS/SKIP) with fixtures that override entity modes in process (pre_option) and read native / flat values with plain SQL, so WP core meta flows run for real - shadow_write suite reproduces the dev36 finding: in shadow_read, update_*_meta returns false and native meta is never written (3 XFAIL marked for PR-056); the dual_write control passes - Move the dev36 integration harness into tests/live/tools; outputs go to cwd / $TMDO_LIVE_OUT, site URL and path are env-configurable - Verify: wp --path= eval-file tests/live/run.php shadow_write → 1 passed, 3 xfail; unit 658 OK, integration 480 OK, phpcs (phpcs.xml) 0, stan OK - PR: PR-052 --- tests/live/README.md | 55 +++++++++ tests/live/lib/assert.php | 130 +++++++++++++++++++++ tests/live/lib/fixtures.php | 178 +++++++++++++++++++++++++++++ tests/live/run.php | 49 ++++++++ tests/live/suites/shadow_write.php | 81 +++++++++++++ tests/live/tools/checker.php | 46 ++++++++ tests/live/tools/clone.sql | 12 ++ tests/live/tools/fe.sh | 19 +++ tests/live/tools/mut.php | 61 ++++++++++ tests/live/tools/qbench.php | 65 +++++++++++ tests/live/tools/qdiff.php | 11 ++ tests/live/tools/run_batch.sh | 14 +++ tests/live/tools/seed.php | 158 +++++++++++++++++++++++++ tests/live/tools/zz-s36-probe.php | 22 ++++ 14 files changed, 901 insertions(+) create mode 100644 tests/live/README.md create mode 100644 tests/live/lib/assert.php create mode 100644 tests/live/lib/fixtures.php create mode 100644 tests/live/run.php create mode 100644 tests/live/suites/shadow_write.php create mode 100644 tests/live/tools/checker.php create mode 100644 tests/live/tools/clone.sql create mode 100644 tests/live/tools/fe.sh create mode 100644 tests/live/tools/mut.php create mode 100644 tests/live/tools/qbench.php create mode 100644 tests/live/tools/qdiff.php create mode 100644 tests/live/tools/run_batch.sh create mode 100644 tests/live/tools/seed.php create mode 100644 tests/live/tools/zz-s36-probe.php diff --git a/tests/live/README.md b/tests/live/README.md new file mode 100644 index 0000000..41668c3 --- /dev/null +++ b/tests/live/README.md @@ -0,0 +1,55 @@ +# Live test suite + +Tests that must run inside a real WordPress site, because the stubbed +`tests/integration` bootstrap cannot reproduce: + +- WP core meta flows (`update_metadata()` reads the old value through + `get_metadata_raw()` before writing — the shadow_read write-loss bug lives there) +- the SQL that `WP_Query` actually builds, and hook priorities +- HivePress / WooCommerce behaviour (`pre_get_posts` sort clauses, empty-key + meta reads, direct-SQL stock updates) + +**Never run against a production site.** Suites create and delete fixtures +(`tmdo_live_*` users, `tmdo-live-*` terms). Entity modes are overridden in +process through `pre_option_wpdo_bridge_modes`; nothing is written to +`wpdo_bridge_modes` or `wpdo_hook_bus_enabled`. + +The reference site is dev36 (HivePress + Marketplace + WooCommerce), with this +plugin symlinked from the `plugins-dev` worktree while developing. + +## Run + +```bash +wp --path=/var/www/Studio/wp-local-dev36 eval-file tests/live/run.php [ ...] +``` + +Exit code 0 when every test passed, 1 otherwise. Output per test: + +| Status | Meaning | +|---|---| +| PASS / FAIL | normal | +| XFAIL | known bug, test marked `xfail` with the PR that fixes it; not counted as a failure | +| XPASS | an `xfail` test passed — counted as a failure so the fixing PR removes the marker | +| SKIP | the site does not register the key / taxonomy the test needs | + +## Suites + +| Suite | Covers | +|---|---| +| `shadow_write` | writes in shadow_read reach native meta (xfail until PR-056) | + +## Tools (`tools/`) + +Benchmark and data-flow helpers used for the 2026-09-26 dev36 integration test. +Output files go to the current directory (`qbench`) or `$TMDO_LIVE_OUT` +(default `/tmp`, `run_batch.sh`), never into the repo. + +| Tool | Use | +|---|---| +| `seed.php` | `wp eval-file seed.php [tag]` — seed through real HivePress / WC APIs | +| `run_batch.sh` | `run_batch.sh [divisor]` — parallel seeding batch (`TMDO_LIVE_WP_PATH`, `TMDO_LIVE_OUT`) | +| `clone.sql` | bench only: clone hp_listing rows to reach large scale (`sed "s/__K__//" clone.sql \| wp db query`) | +| `checker.php` | `wp eval-file checker.php [entity]` — native vs flat consistency per registered key (SQL only) | +| `qbench.php` / `qdiff.php` | run 16 WP_Query shapes, then diff two runs (native vs routed): result sets, order, latency, SQL errors | +| `mut.php` | data-flow mutations through HivePress models, WC CRUD and core APIs; prints native / flat / API values | +| `fe.sh` + `zz-s36-probe.php` | front-end search probe (`TMDO_LIVE_URL`). Copy the probe into `wp-content/mu-plugins/` only for the run and remove it afterwards; it only answers requests carrying `X-S36-Probe: 1` | diff --git a/tests/live/lib/assert.php b/tests/live/lib/assert.php new file mode 100644 index 0000000..50c1f6f --- /dev/null +++ b/tests/live/lib/assert.php @@ -0,0 +1,130 @@ + + */ + private static array $tests = array(); + + /** + * Suite currently being loaded. + * + * @var string + */ + public static string $suite = ''; + + /** + * Register a test. + * + * `xfail` marks a known bug: a failure is reported as XFAIL (not counted), + * a pass is reported as XPASS and counted as a failure so the marker gets + * removed by the PR that fixes the bug. + * + * @param string $name Test name. + * @param callable $fn Test body. + * @param array $opts Options: xfail => reason. + */ + public static function test( string $name, callable $fn, array $opts = array() ): void { + self::$tests[] = array( + 'suite' => self::$suite, + 'name' => $name, + 'fn' => $fn, + 'xfail' => (string) ( $opts['xfail'] ?? '' ), + ); + } + + /** + * Run every registered test; return true when nothing failed. + */ + public static function run(): bool { + $counts = array( 'PASS' => 0, 'FAIL' => 0, 'XFAIL' => 0, 'XPASS' => 0, 'SKIP' => 0 ); + + foreach ( self::$tests as $t ) { + $detail = ''; + try { + ( $t['fn'] )(); + $status = '' !== $t['xfail'] ? 'XPASS' : 'PASS'; + } catch ( TMDO_Live_Skip $e ) { + $status = 'SKIP'; + $detail = $e->getMessage(); + } catch ( Throwable $e ) { + $status = '' !== $t['xfail'] ? 'XFAIL' : 'FAIL'; + $detail = ( $e instanceof TMDO_Live_Failure ? '' : get_class( $e ) . ': ' ) . $e->getMessage(); + } finally { + TMDO_Live_Fixtures::teardown(); + } + + ++$counts[ $status ]; + if ( '' !== $t['xfail'] ) { + $detail = trim( $detail . ' [xfail: ' . $t['xfail'] . ']' ); + } + printf( "%-5s %s :: %s%s\n", $status, $t['suite'], $t['name'], '' !== $detail ? "\n " . $detail : '' ); + } + + printf( + "\n%d passed, %d failed, %d xfail, %d xpass, %d skipped\n", + $counts['PASS'], + $counts['FAIL'], + $counts['XFAIL'], + $counts['XPASS'], + $counts['SKIP'] + ); + + return 0 === $counts['FAIL'] && 0 === $counts['XPASS']; + } +} + +/** + * Fail unless $actual is identical to $expected. + * + * @param mixed $expected Expected value. + * @param mixed $actual Actual value. + * @param string $message Context. + */ +function tmdo_live_assert_same( $expected, $actual, string $message ): void { + if ( $expected !== $actual ) { + throw new TMDO_Live_Failure( + sprintf( '%s — expected %s, got %s', $message, var_export( $expected, true ), var_export( $actual, true ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export + ); + } +} + +/** + * Fail unless $condition is true. + * + * @param bool $condition Condition. + * @param string $message Context. + */ +function tmdo_live_assert_true( bool $condition, string $message ): void { + if ( ! $condition ) { + throw new TMDO_Live_Failure( $message ); + } +} diff --git a/tests/live/lib/fixtures.php b/tests/live/lib/fixtures.php new file mode 100644 index 0000000..6a31654 --- /dev/null +++ b/tests/live/lib/fixtures.php @@ -0,0 +1,178 @@ + + */ + private static array $created = array(); + + /** + * Active pre_option callback, if any. + * + * @var callable|null + */ + private static $mode_filter = null; + + /** + * Override entity modes for this process and make sure the Hook Bus is hooked. + * + * @param array $modes e.g. array( 'user' => 'shadow_read' ). + */ + public static function use_modes( array $modes ): void { + self::clear_mode_filter(); + + $stored = get_option( 'wpdo_bridge_modes', array() ); + $merged = array_merge( is_array( $stored ) ? $stored : array(), $modes ); + self::$mode_filter = static function () use ( $merged ) { + return $merged; + }; + add_filter( 'pre_option_wpdo_bridge_modes', self::$mode_filter ); + self::reset_caches(); + + // The Hook Bus only boots when wpdo_hook_bus_enabled is on; the suite + // must not depend on (or change) that site option. + if ( false === has_filter( 'update_user_metadata', array( 'TMDO_Hook_Bus', 'intercept_update' ) ) ) { + TMDO_Hook_Bus::init(); + } + } + + /** + * Create a throwaway user. + */ + public static function user(): int { + $login = 'tmdo_live_' . wp_generate_password( 6, false, false ); + $id = wp_insert_user( + array( + 'user_login' => $login, + 'user_email' => $login . '@example.test', + 'user_pass' => wp_generate_password(), + ) + ); + if ( is_wp_error( $id ) ) { + throw new TMDO_Live_Failure( 'fixture user: ' . $id->get_error_message() ); + } + self::$created[] = array( 'user', (int) $id ); + return (int) $id; + } + + /** + * Create a throwaway term. + * + * @param string $taxonomy Taxonomy. + */ + public static function term( string $taxonomy ): int { + if ( ! taxonomy_exists( $taxonomy ) ) { + throw new TMDO_Live_Skip( "taxonomy {$taxonomy} not registered on this site" ); + } + $t = wp_insert_term( 'tmdo-live-' . wp_generate_password( 6, false, false ), $taxonomy ); + if ( is_wp_error( $t ) ) { + throw new TMDO_Live_Failure( 'fixture term: ' . $t->get_error_message() ); + } + self::$created[] = array( 'term', (int) $t['term_id'], $taxonomy ); + return (int) $t['term_id']; + } + + /** + * Native meta values for a key, read with SQL (no filters, no cache). + * + * @param string $type Entity type (post|user|term|comment). + * @param int $id Entity ID. + * @param string $key Meta key. + * @return string[] + */ + public static function native( string $type, int $id, string $key ): array { + global $wpdb; + $table = _get_meta_table( $type ); + $id_col = $type . '_id'; + $pk = 'user' === $type ? 'umeta_id' : 'meta_id'; + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column names are WP internals. + return $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$table} WHERE {$id_col} = %d AND meta_key = %s ORDER BY {$pk}", $id, $key ) ); + } + + /** + * Raw flat-table value for a key; 'NO-ROW' when the entity has no row. + * + * @param string $type Entity type. + * @param int $id Entity ID. + * @param string $key Meta key. + * @return string|null + */ + public static function flat( string $type, int $id, string $key ) { + global $wpdb; + $field = self::field( $type, $key ); + $table = TMDO_Schema_Manager::get_table_name( $type, $field['group'] ); + $col = TMDO_Schema_Manager::sanitize_column_name( $key ); + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column come from the registry. + $row = $wpdb->get_row( $wpdb->prepare( "SELECT `{$col}` AS v FROM `{$table}` WHERE {$type}_id = %d", $id ), ARRAY_A ); + return null === $row ? 'NO-ROW' : $row['v']; + } + + /** + * Registry field for a key, or skip the test when the site does not register it. + * + * @param string $type Entity type. + * @param string $key Meta key. + */ + public static function field( string $type, string $key ): array { + $field = TMDO_Entity_Registry::get_field( $type, $key ); + if ( ! $field ) { + throw new TMDO_Live_Skip( "{$type} key {$key} is not registered on this site" ); + } + if ( ! TMDO_Schema_Manager::table_exists( TMDO_Schema_Manager::get_table_name( $type, $field['group'] ) ) ) { + throw new TMDO_Live_Skip( "flat table for {$type}/{$field['group']} does not exist" ); + } + return $field; + } + + /** + * Delete fixtures and drop the mode override. + */ + public static function teardown(): void { + require_once ABSPATH . 'wp-admin/includes/user.php'; + foreach ( array_reverse( self::$created ) as $c ) { + if ( 'user' === $c[0] ) { + wp_delete_user( $c[1] ); + } elseif ( 'term' === $c[0] ) { + wp_delete_term( $c[1], $c[2] ); + } + } + self::$created = array(); + self::clear_mode_filter(); + self::reset_caches(); + } + + /** + * Remove the pre_option override, if any. + */ + private static function clear_mode_filter(): void { + if ( null !== self::$mode_filter ) { + remove_filter( 'pre_option_wpdo_bridge_modes', self::$mode_filter ); + self::$mode_filter = null; + } + } + + /** + * Reset request-level caches that memoise modes or routing decisions. + */ + private static function reset_caches(): void { + TMDO_Mode_Manager::reset_cache(); + TMDO_Routing_Predicate::flush_cache(); + } +} diff --git a/tests/live/run.php b/tests/live/run.php new file mode 100644 index 0000000..09a76f9 --- /dev/null +++ b/tests/live/run.php @@ -0,0 +1,49 @@ + eval-file tests/live/run.php [ ...] + * + * Exit code 0 when every test passed (XFAIL allowed), 1 otherwise. + * Never run against a production site: suites create and delete fixtures. + * + * No strict_types here: wp eval-file evals this file, and a declare must be + * the first statement of a real file. The required files declare it. + * + * @package WP_Data_Optimizer + */ + +require __DIR__ . '/lib/assert.php'; +require __DIR__ . '/lib/fixtures.php'; + +$tmdo_live_suites = isset( $args ) ? (array) $args : array(); + +if ( ! class_exists( 'TMDO_Hook_Bus' ) ) { + fwrite( STDERR, "2meet-data-optimizer is not active on this site.\n" ); + exit( 1 ); +} + +if ( empty( $tmdo_live_suites ) ) { + $tmdo_live_available = array_map( + static function ( $f ) { + return basename( $f, '.php' ); + }, + glob( __DIR__ . '/suites/*.php' ) + ); + fwrite( STDERR, 'Usage: wp eval-file tests/live/run.php ... Suites: ' . implode( ', ', $tmdo_live_available ) . "\n" ); + exit( 1 ); +} + +foreach ( $tmdo_live_suites as $tmdo_live_suite ) { + $tmdo_live_file = __DIR__ . '/suites/' . basename( (string) $tmdo_live_suite ) . '.php'; + if ( ! is_file( $tmdo_live_file ) ) { + fwrite( STDERR, "Unknown suite: {$tmdo_live_suite}\n" ); + exit( 1 ); + } + TMDO_Live::$suite = (string) $tmdo_live_suite; + require $tmdo_live_file; +} + +exit( TMDO_Live::run() ? 0 : 1 ); diff --git a/tests/live/suites/shadow_write.php b/tests/live/suites/shadow_write.php new file mode 100644 index 0000000..dc51062 --- /dev/null +++ b/tests/live/suites/shadow_write.php @@ -0,0 +1,81 @@ + 'dual_write' ) ); + $uid = TMDO_Live_Fixtures::user(); + update_user_meta( $uid, 'first_name', 'Before' ); + + $ret = update_user_meta( $uid, 'first_name', 'After' ); + + tmdo_live_assert_true( false !== $ret, 'update_user_meta returned false' ); + tmdo_live_assert_same( array( 'After' ), TMDO_Live_Fixtures::native( 'user', $uid, 'first_name' ), 'native first_name' ); + tmdo_live_assert_same( 'After', TMDO_Live_Fixtures::flat( 'user', $uid, 'first_name' ), 'flat first_name' ); + } +); + +TMDO_Live::test( + 'shadow_read: user first_name update reaches native', + static function () { + TMDO_Live_Fixtures::field( 'user', 'first_name' ); + TMDO_Live_Fixtures::use_modes( array( 'user' => 'dual_write' ) ); + $uid = TMDO_Live_Fixtures::user(); + update_user_meta( $uid, 'first_name', 'Before' ); + TMDO_Live_Fixtures::use_modes( array( 'user' => 'shadow_read' ) ); + + $ret = update_user_meta( $uid, 'first_name', 'After' ); + + tmdo_live_assert_true( false !== $ret, 'update_user_meta returned false' ); + tmdo_live_assert_same( array( 'After' ), TMDO_Live_Fixtures::native( 'user', $uid, 'first_name' ), 'native first_name' ); + }, + array( 'xfail' => 'PR-056' ) +); + +TMDO_Live::test( + 'shadow_read → dual_write: value written in shadow_read survives demotion', + static function () { + TMDO_Live_Fixtures::field( 'user', 'last_name' ); + TMDO_Live_Fixtures::use_modes( array( 'user' => 'dual_write' ) ); + $uid = TMDO_Live_Fixtures::user(); + update_user_meta( $uid, 'last_name', 'Before' ); + TMDO_Live_Fixtures::use_modes( array( 'user' => 'shadow_read' ) ); + update_user_meta( $uid, 'last_name', 'After' ); + + TMDO_Live_Fixtures::use_modes( array( 'user' => 'dual_write' ) ); + wp_cache_delete( $uid, 'user_meta' ); + + tmdo_live_assert_same( 'After', get_user_meta( $uid, 'last_name', true ), 'last_name after demotion' ); + }, + array( 'xfail' => 'PR-056' ) +); + +TMDO_Live::test( + 'shadow_read: HivePress term hp_icon update reaches native', + static function () { + TMDO_Live_Fixtures::field( 'term', 'hp_icon' ); + TMDO_Live_Fixtures::use_modes( array( 'term' => 'dual_write' ) ); + $tid = TMDO_Live_Fixtures::term( 'hp_listing_category' ); + update_term_meta( $tid, 'hp_icon', 'star' ); + TMDO_Live_Fixtures::use_modes( array( 'term' => 'shadow_read' ) ); + + $ret = update_term_meta( $tid, 'hp_icon', 'heart' ); + + tmdo_live_assert_true( false !== $ret, 'update_term_meta returned false' ); + tmdo_live_assert_same( array( 'heart' ), TMDO_Live_Fixtures::native( 'term', $tid, 'hp_icon' ), 'native hp_icon' ); + }, + array( 'xfail' => 'PR-056' ) +); diff --git a/tests/live/tools/checker.php b/tests/live/tools/checker.php new file mode 100644 index 0000000..b7df022 --- /dev/null +++ b/tests/live/tools/checker.php @@ -0,0 +1,46 @@ + 'post_id', 'user' => 'user_id', 'term' => 'term_id', 'comment' => 'comment_id' ); +$metas = array( 'post' => $wpdb->postmeta, 'user' => $wpdb->usermeta, 'term' => $wpdb->termmeta, 'comment' => $wpdb->commentmeta ); +$tot = array( 'miss' => 0, 'diff' => 0, 'extra' => 0 ); + +printf( "%-8s %-24s %-28s %8s %8s %6s %6s %6s\n", 'entity', 'group', 'key', 'native', 'flat', 'miss', 'diff', 'extra' ); +foreach ( $ids as $e => $idcol ) { + if ( $only && $only !== $e ) { continue; } + $mt = $metas[ $e ]; + $mid = ( 'user' === $e ) ? 'umeta_id' : 'meta_id'; + foreach ( TMDO_Entity_Registry::get_groups_for_type( $e ) as $g ) { + $table = $wpdb->prefix . 'wpdo_' . $e . '_' . $g; + if ( ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) ) { continue; } + $scope = ( 'post' === $e ) ? TMDO_Entity_Registry::get_group_post_types( $e, $g ) : array(); + $scope_sql = $scope ? " AND m.post_id IN (SELECT ID FROM {$wpdb->posts} WHERE post_type IN ('" . implode( "','", array_map( 'esc_sql', $scope ) ) . "'))" : ''; + foreach ( TMDO_Entity_Registry::get_group_keys( $e, $g ) as $key ) { + $col = preg_replace( '/[^a-zA-Z0-9_]/', '', $key ); + // Latest native value per entity. + $nat = $wpdb->prepare( "SELECT m.$idcol AS eid, m.meta_value AS v FROM $mt m JOIN (SELECT $idcol, MAX($mid) mid FROM $mt WHERE meta_key=%s GROUP BY $idcol) x ON x.mid=m.$mid WHERE 1=1 $scope_sql", $key ); + $n_native = (int) $wpdb->get_var( "SELECT COUNT(*) FROM ($nat) n" ); + $n_flat = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table WHERE `$col` IS NOT NULL" ); + $miss = (int) $wpdb->get_var( "SELECT COUNT(*) FROM ($nat) n LEFT JOIN $table f ON f.$idcol=n.eid WHERE f.$idcol IS NULL OR (f.`$col` IS NULL AND n.v <> '')" ); + // Value diff: numeric-aware loose compare. + $diff = (int) $wpdb->get_var( "SELECT COUNT(*) FROM ($nat) n JOIN $table f ON f.$idcol=n.eid WHERE f.`$col` IS NOT NULL AND NOT ( + CAST(f.`$col` AS CHAR) = n.v + OR (n.v REGEXP '^-?[0-9]+(\\\\.[0-9]+)?$' AND CAST(f.`$col` AS DECIMAL(30,6)) = CAST(n.v AS DECIMAL(30,6))) + OR (n.v='' AND CAST(f.`$col` AS CHAR) IN ('','0')) + )" ); + $extra = (int) $wpdb->get_var( "SELECT COUNT(*) FROM $table f WHERE f.`$col` IS NOT NULL AND NOT EXISTS (SELECT 1 FROM $mt m WHERE m.$idcol=f.$idcol AND m.meta_key='" . esc_sql( $key ) . "')" ); + if ( $n_native || $n_flat ) { + printf( "%-8s %-24s %-28s %8d %8d %6d %6d %6d\n", $e, $g, $key, $n_native, $n_flat, $miss, $diff, $extra ); + } + $tot['miss'] += $miss; + $tot['diff'] += $diff; + $tot['extra'] += $extra; + } + } +} +printf( "TOTAL miss=%d diff=%d extra=%d\n", $tot['miss'], $tot['diff'], $tot['extra'] ); diff --git a/tests/live/tools/clone.sql b/tests/live/tools/clone.sql new file mode 100644 index 0000000..15fbfc1 --- /dev/null +++ b/tests/live/tools/clone.sql @@ -0,0 +1,12 @@ +-- Clone original hp_listing rows (ID < 20000) into copy k with ID offset k*20000. +-- Bench-only: clones hp_listing rows into offset IDs. Never run on a production site. +-- Usage: sed "s/__K__//" clone.sql | wp db query +SET @k := __K__; SET @off := @k * 20000; +INSERT INTO wp_posts (ID, post_author, post_date, post_date_gmt, post_content, post_title, post_excerpt, post_status, comment_status, ping_status, post_password, post_name, to_ping, pinged, post_modified, post_modified_gmt, post_content_filtered, post_parent, guid, menu_order, post_type, post_mime_type, comment_count) +SELECT ID+@off, post_author, d, d, post_content, CONCAT(post_title,' c',@k), post_excerpt, post_status, comment_status, ping_status, post_password, CONCAT(post_name,'-c',@k), to_ping, pinged, d, d, post_content_filtered, post_parent, CONCAT(guid,'&c=',@k), menu_order, post_type, post_mime_type, comment_count +FROM (SELECT *, DATE_SUB('2026-09-26 12:00:00', INTERVAL FLOOR(RAND()*63072000) SECOND) d FROM wp_posts WHERE post_type='hp_listing' AND ID < 20000) s; +INSERT INTO wp_postmeta (post_id, meta_key, meta_value) +SELECT m.post_id+@off, m.meta_key, CASE m.meta_key WHEN 'hp_price' THEN FLOOR(100+RAND()*49900) WHEN 'hp_hourly_rate' THEN FLOOR(10+RAND()*290) ELSE m.meta_value END +FROM wp_postmeta m JOIN wp_posts p ON p.ID=m.post_id WHERE p.post_type='hp_listing' AND p.ID < 20000 AND m.meta_key NOT LIKE '\_transient%'; +INSERT INTO wp_term_relationships (object_id, term_taxonomy_id, term_order) +SELECT r.object_id+@off, r.term_taxonomy_id, r.term_order FROM wp_term_relationships r JOIN wp_posts p ON p.ID=r.object_id WHERE p.post_type='hp_listing' AND p.ID < 20000; diff --git a/tests/live/tools/fe.sh b/tests/live/tools/fe.sh new file mode 100644 index 0000000..66e3eb0 --- /dev/null +++ b/tests/live/tools/fe.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# usage: fe.sh