test(2meet-data-optimizer): add live test runner and failing shadow_read write suite
- 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=<dev36> 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
This commit is contained in:
@@ -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 <suite> [<suite> ...]
|
||||
```
|
||||
|
||||
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 <vendors\|listings\|requests\|reviews\|products\|orders\|terms> <n> [tag]` — seed through real HivePress / WC APIs |
|
||||
| `run_batch.sh` | `run_batch.sh <tag> [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__/<n>/" 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` |
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal assertion + test registry for the live suite.
|
||||
*
|
||||
* Why not PHPUnit: the live suite runs inside a real WordPress request
|
||||
* (`wp eval-file`) so WP core's update_metadata() flow, WP_Query SQL and
|
||||
* third-party hooks (HivePress, WooCommerce) are exercised for real — the
|
||||
* stubbed integration bootstrap cannot reproduce those.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Thrown by the assert helpers.
|
||||
*/
|
||||
class TMDO_Live_Failure extends RuntimeException {}
|
||||
|
||||
/**
|
||||
* Thrown to skip a test whose preconditions the site does not meet.
|
||||
*/
|
||||
class TMDO_Live_Skip extends RuntimeException {}
|
||||
|
||||
/**
|
||||
* Test registry and runner.
|
||||
*/
|
||||
final class TMDO_Live {
|
||||
|
||||
/**
|
||||
* Registered tests.
|
||||
*
|
||||
* @var array<int, array{suite:string, name:string, fn:callable, xfail:string}>
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* Live-suite fixtures: in-process mode overrides, throwaway entities, and
|
||||
* direct reads of native meta / flat tables (bypassing every meta filter).
|
||||
*
|
||||
* Nothing here persists a mode change: modes are overridden through the
|
||||
* `pre_option_wpdo_bridge_modes` filter for the current process only.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Fixture helpers, reset after every test by TMDO_Live::run().
|
||||
*/
|
||||
final class TMDO_Live_Fixtures {
|
||||
|
||||
/**
|
||||
* Entities created by the current test, deleted on teardown.
|
||||
*
|
||||
* @var array<int, array{0:string, 1:int, 2?:string}>
|
||||
*/
|
||||
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<string, string> $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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Live test runner — runs suites inside a real WordPress site (HivePress /
|
||||
* WooCommerce active) so WP core meta flows and query SQL are exercised for real.
|
||||
*
|
||||
* Usage:
|
||||
* wp --path=<site> eval-file tests/live/run.php <suite> [<suite> ...]
|
||||
*
|
||||
* 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 <suite> ... 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 );
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Suite: writes made in shadow_read must reach native meta.
|
||||
*
|
||||
* Bug (dev36, 2026-09-26): in shadow_read, update_metadata() fetches the old
|
||||
* value through get_metadata_raw(); the Hook Bus has already upserted the new
|
||||
* value and serves it from the flat table, so WP sees old === new and returns
|
||||
* false without writing native meta. Demoting afterwards exposes stale data.
|
||||
*
|
||||
* @package WP_Data_Optimizer
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
TMDO_Live::test(
|
||||
'dual_write: user first_name update reaches native and flat (control)',
|
||||
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' );
|
||||
|
||||
$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' )
|
||||
);
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Independent native-meta vs flat-table consistency checker (does not use TMDO read paths).
|
||||
* Usage: wp eval-file checker.php [entity]
|
||||
* Per registered group/key: native rows, flat non-null, missing-in-flat, value mismatches, extra-in-flat.
|
||||
*/
|
||||
global $wpdb;
|
||||
$only = $args[0] ?? '';
|
||||
$ids = array( 'post' => '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'] );
|
||||
@@ -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__/<n>/" 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;
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# usage: fe.sh <label> — front-end HivePress/WC search probe, 5 runs each, median ms
|
||||
B="${TMDO_LIVE_URL:-https://dev36.2meet.pro}"
|
||||
URLS=(
|
||||
"/?post_type=hp_listing&s=&price[]=10000&price[]=20000"
|
||||
"/?post_type=hp_listing&s=&price[]=10000&price[]=20000&_sort=price__asc"
|
||||
"/?post_type=hp_listing&s=&hourly_rate[]=10&hourly_rate[]=50&_sort=hourly_rate__desc"
|
||||
"/?post_type=hp_listing&s=&_sort=rating__desc"
|
||||
"/?post_type=hp_listing&s=Taipei&price[]=1000&price[]=30000"
|
||||
"/?post_type=hp_vendor&s=&hourly_rate[]=100&hourly_rate[]=200"
|
||||
"/?post_type=hp_request&s="
|
||||
"/shop/?min_price=1000&max_price=5000"
|
||||
"/shop/?orderby=price-desc"
|
||||
)
|
||||
for u in "${URLS[@]}"; do
|
||||
ms=(); for i in 1 2 3 4 5; do h=$(curl -s -o /dev/null -D - -H "X-S36-Probe: 1" "$B$u"); ms+=($(echo "$h" | grep -i x-s36-ms | awk "{print \$2}" | tr -dc 0-9)); done
|
||||
med=$(printf "%s\n" "${ms[@]}" | sort -n | sed -n 3p)
|
||||
echo "$u|$(echo "$h" | grep -i x-s36-found | awk "{print \$2}" | tr -dc 0-9)|$(echo "$h" | grep -i x-s36-ids | cut -d' ' -f2 | tr -d '\r')|$(echo "$h" | grep -i x-s36-queries | awk "{print \$2}" | tr -dc 0-9)|$med"
|
||||
done
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
// Data-flow mutation test. Usage: wp eval-file mut.php <label>
|
||||
use HivePress\Models;
|
||||
add_filter( 'pre_http_request', function ( $r, $a, $u ) { return false !== strpos( $u, 'pixel.wp.com' ) ? array( 'headers' => array(), 'body' => '', 'response' => array( 'code' => 200, 'message' => 'OK' ), 'cookies' => array() ) : $r; }, 1, 3 );
|
||||
global $wpdb;
|
||||
$mode = TMDO_Mode_Manager::get( 'post' ) . '/' . TMDO_Mode_Manager::get( 'user' );
|
||||
function nat( $t, $id, $k ) { global $wpdb; $mt = $t === 'user' ? $wpdb->usermeta : $wpdb->postmeta; $c = $t === 'user' ? 'user_id' : 'post_id'; $v = $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM $mt WHERE $c=%d AND meta_key=%s", $id, $k ) ); return $v ? implode( '|', $v ) : '∅'; }
|
||||
function flat( $t, $g, $id, $k ) { global $wpdb; $c = $t === 'user' ? 'user_id' : 'post_id'; $v = $wpdb->get_row( $wpdb->prepare( "SELECT `$k` v FROM {$wpdb->prefix}wpdo_{$t}_{$g} WHERE $c=%d", $id ) ); return $v ? ( $v->v ?? 'NULL' ) : 'NO-ROW'; }
|
||||
function row( $name, $vals ) { printf( "%-44s %s\n", $name, implode( ' ', array_map( fn( $k, $v ) => "$k=" . ( is_scalar( $v ) || is_null( $v ) ? var_export( $v, true ) : json_encode( $v ) ), array_keys( $vals ), $vals ) ) ); }
|
||||
function clear_caches( $id ) { wp_cache_flush(); clean_post_cache( $id ); }
|
||||
echo "== mode post/user = $mode\n";
|
||||
|
||||
// T1: HivePress model update of a listing price.
|
||||
$lid = (int) $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_type='hp_listing' AND post_status='publish' ORDER BY ID DESC LIMIT 1" );
|
||||
$new = mt_rand( 60000, 69999 );
|
||||
update_post_meta( $lid, 'hp_price', $new ); // HP stores attributes via meta API
|
||||
clear_caches( $lid );
|
||||
$m = Models\Listing::query()->get_by_id( $lid );
|
||||
row( "T1 listing $lid hp_price:=$new", array( 'native' => nat( 'post', $lid, 'hp_price' ), 'flat' => flat( 'post', 'hp_listing_core', $lid, 'hp_price' ), 'get_meta' => get_post_meta( $lid, 'hp_price', true ), 'all_meta' => get_post_meta( $lid )['hp_price'][0] ?? '∅' ) );
|
||||
|
||||
// T2: HivePress model field (verified) via model save.
|
||||
$m->set_verified( ! $m->is_verified() )->save( array( 'verified' ) );
|
||||
clear_caches( $lid );
|
||||
$m2 = Models\Listing::query()->get_by_id( $lid );
|
||||
row( "T2 listing verified via HP model", array( 'native' => nat( 'post', $lid, 'hp_verified' ), 'flat' => flat( 'post', 'hp_listing_core', $lid, 'hp_verified' ), 'hp_model' => $m2->is_verified(), 'get_meta' => get_post_meta( $lid, 'hp_verified', true ) ) );
|
||||
|
||||
// T3: WC product price via CRUD.
|
||||
$pid = (int) $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_type='product' AND post_title LIKE 'Product B1%' ORDER BY ID DESC LIMIT 1" );
|
||||
$p = wc_get_product( $pid ); $np = (string) mt_rand( 7000, 7999 ); $p->set_regular_price( $np ); $p->set_sale_price( '' ); $p->save();
|
||||
clear_caches( $pid );
|
||||
row( "T3 product $pid _regular_price:=$np", array( 'native' => nat( 'post', $pid, '_regular_price' ), 'flat' => flat( 'post', 'wc_product', $pid, '_regular_price' ), 'wc_read' => wc_get_product( $pid )->get_regular_price(), 'lookup' => $wpdb->get_var( "SELECT min_price FROM {$wpdb->prefix}wc_product_meta_lookup WHERE product_id=$pid" ) ) );
|
||||
|
||||
// T4: order reduces stock (WC direct SQL path).
|
||||
$before = wc_get_product( $pid )->get_stock_quantity();
|
||||
$o = wc_create_order(); $o->add_product( wc_get_product( $pid ), 2 ); $o->calculate_totals( false ); $o->set_status( 'processing' ); $o->save(); wc_reduce_stock_levels( $o->get_id() );
|
||||
clear_caches( $pid );
|
||||
row( "T4 order x2 on product $pid (stock $before)", array( 'native' => nat( 'post', $pid, '_stock' ), 'flat' => flat( 'post', 'wc_product', $pid, '_stock' ), 'get_meta' => get_post_meta( $pid, '_stock', true ), 'wc_read' => wc_get_product( $pid )->get_stock_quantity(), 'sales_native' => nat( 'post', $pid, 'total_sales' ), 'sales_flat' => flat( 'post', 'wc_product', $pid, 'total_sales' ) ) );
|
||||
|
||||
// T5: delete a meta key.
|
||||
update_post_meta( $lid, 'hp_featured', 1 ); delete_post_meta( $lid, 'hp_featured' ); clear_caches( $lid );
|
||||
row( "T5 delete_post_meta hp_featured", array( 'native' => nat( 'post', $lid, 'hp_featured' ), 'flat' => flat( 'post', 'hp_listing_core', $lid, 'hp_featured' ), 'get_meta' => var_export( get_post_meta( $lid, 'hp_featured', true ), true ), 'exists' => metadata_exists( 'post', $lid, 'hp_featured' ) ) );
|
||||
|
||||
// T6: user first_name via wp_update_user.
|
||||
$uid = (int) $wpdb->get_var( "SELECT ID FROM {$wpdb->users} WHERE user_login LIKE 's36v_B1p1_%' ORDER BY ID LIMIT 1" );
|
||||
$fn = 'Fn' . mt_rand( 100, 999 ); wp_update_user( array( 'ID' => $uid, 'first_name' => $fn ) ); wp_cache_flush();
|
||||
$u = get_userdata( $uid );
|
||||
row( "T6 user $uid first_name:=$fn", array( 'native' => nat( 'user', $uid, 'first_name' ), 'flat' => flat( 'user', 'core_profile', $uid, 'first_name' ), 'get_meta' => get_user_meta( $uid, 'first_name', true ), 'WP_User' => $u->first_name, 'wc_customer' => ( new WC_Customer( $uid ) )->get_first_name() ) );
|
||||
|
||||
// T7: WC customer billing city via CRUD.
|
||||
$c = new WC_Customer( $uid ); $city = 'City' . mt_rand( 10, 99 ); $c->set_billing_city( $city ); $c->save(); wp_cache_flush();
|
||||
row( "T7 WC_Customer billing_city:=$city", array( 'native' => nat( 'user', $uid, 'billing_city' ), 'flat' => flat( 'user', 'commerce', $uid, 'billing_city' ), 'get_meta' => get_user_meta( $uid, 'billing_city', true ), 'wc_customer' => ( new WC_Customer( $uid ) )->get_billing_city() ) );
|
||||
|
||||
// T8: delete a listing → flat row?
|
||||
$d = (int) $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_type='hp_listing' AND post_status='draft' ORDER BY ID DESC LIMIT 1" );
|
||||
wp_delete_post( $d, true );
|
||||
row( "T8 wp_delete_post $d", array( 'posts' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE ID=$d" ), 'native_meta' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE post_id=$d" ), 'flat_row' => flat( 'post', 'hp_listing_core', $d, 'post_id' ) ) );
|
||||
|
||||
// T9: HP review → listing rating recompute.
|
||||
$before = get_post_meta( $lid, 'hp_rating_count', true );
|
||||
$rv = ( new Models\Review() )->fill( array( 'text' => 'mut', 'rating' => 5, 'approved' => 1, 'author' => $uid, 'author__display_name' => 'x', 'author__email' => 'x@example.test', 'listing' => $lid ) ); $rv->save(); wp_cache_flush();
|
||||
row( "T9 add review (count was $before)", array( 'native' => nat( 'post', $lid, 'hp_rating_count' ), 'flat' => flat( 'post', 'hp_listing_core', $lid, 'hp_rating_count' ), 'hp_model' => Models\Listing::query()->get_by_id( $lid )->get_rating_count() ) );
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* Query correctness + latency harness. Runs identical WP_Query shapes and dumps
|
||||
* ids / found_posts / timings to JSON so two runs (native vs routed mode) can be diffed.
|
||||
* Usage: wp eval-file qbench.php <label> [iterations]
|
||||
*/
|
||||
global $wpdb;
|
||||
$label = $args[0] ?? 'run';
|
||||
$iters = (int) ( $args[1] ?? 5 );
|
||||
$out = getcwd() . "/qbench_{$label}.json";
|
||||
|
||||
// Deterministic parameters derived from data (same in every run).
|
||||
$vendor_ids = array_map( 'intval', $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_type='hp_vendor' AND post_status='publish' ORDER BY ID LIMIT 3" ) );
|
||||
|
||||
$L = array( 'post_type' => 'hp_listing', 'post_status' => 'publish', 'posts_per_page' => 20 );
|
||||
$cases = array(
|
||||
'Q01 price BETWEEN (NUMERIC)' => $L + array( 'meta_query' => array( array( 'key' => 'hp_price', 'value' => array( 10000, 20000 ), 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ) ) ),
|
||||
'Q02 price >= AND verified=1' => $L + array( 'meta_query' => array( 'relation' => 'AND', array( 'key' => 'hp_price', 'value' => 25000, 'compare' => '>=', 'type' => 'NUMERIC' ), array( 'key' => 'hp_verified', 'value' => '1' ) ) ),
|
||||
'Q03 rating >= 4.5 (DECIMAL)' => $L + array( 'meta_query' => array( array( 'key' => 'hp_rating', 'value' => '4.5', 'compare' => '>=', 'type' => 'DECIMAL(10,2)' ) ) ),
|
||||
'Q04 featured=1 OR verified=1' => $L + array( 'meta_query' => array( 'relation' => 'OR', array( 'key' => 'hp_featured', 'value' => '1' ), array( 'key' => 'hp_verified', 'value' => '1' ) ) ),
|
||||
'Q05 rate>=100 AND _thumbnail EXISTS'=> $L + array( 'meta_query' => array( array( 'key' => 'hp_hourly_rate', 'value' => 100, 'compare' => '>=', 'type' => 'NUMERIC' ), array( 'key' => '_thumbnail_id', 'compare' => 'EXISTS' ) ) ),
|
||||
'Q06 product _sku LIKE' => array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 20, 'meta_query' => array( array( 'key' => '_sku', 'value' => 'S36-B1-1', 'compare' => 'LIKE' ) ) ),
|
||||
'Q07 orderby meta_value_num price' => $L + array( 'meta_key' => 'hp_price', 'orderby' => 'meta_value_num', 'order' => 'DESC' ),
|
||||
'Q08 multi post_type + hp_budget' => array( 'post_type' => array( 'hp_listing', 'hp_request' ), 'post_status' => 'publish', 'posts_per_page' => 20, 'meta_query' => array( array( 'key' => 'hp_budget', 'value' => 80000, 'compare' => '>=', 'type' => 'NUMERIC' ) ) ),
|
||||
'Q09 featured NOT EXISTS' => $L + array( 'meta_query' => array( array( 'key' => 'hp_featured', 'compare' => 'NOT EXISTS' ) ) ),
|
||||
'Q10 rating_count != 1' => $L + array( 'meta_query' => array( array( 'key' => 'hp_rating_count', 'value' => '1', 'compare' => '!=' ) ) ),
|
||||
'Q11 product _price BETWEEN+instock' => array( 'post_type' => 'product', 'post_status' => 'publish', 'posts_per_page' => 20, 'meta_query' => array( array( 'key' => '_price', 'value' => array( 1000, 5000 ), 'compare' => 'BETWEEN', 'type' => 'NUMERIC' ), array( 'key' => '_stock_status', 'value' => 'instock' ) ) ),
|
||||
'Q12 request budget > (NUMERIC)' => array( 'post_type' => 'hp_request', 'post_status' => 'publish', 'posts_per_page' => 20, 'meta_query' => array( array( 'key' => 'hp_budget', 'value' => 50000, 'compare' => '>', 'type' => 'NUMERIC' ) ) ),
|
||||
'Q13 rating_count IN (1,2)' => $L + array( 'meta_query' => array( array( 'key' => 'hp_rating_count', 'value' => array( 1, 2 ), 'compare' => 'IN' ) ) ),
|
||||
'Q16 price<1000 OR warranty=0 (mix)' => $L + array( 'meta_query' => array( 'relation' => 'OR', array( 'key' => 'hp_price', 'value' => 1000, 'compare' => '<', 'type' => 'NUMERIC' ), array( 'key' => 'hp_warranty', 'value' => '0' ) ) ),
|
||||
'Q14 price > (CHAR, no type)' => $L + array( 'meta_query' => array( array( 'key' => 'hp_price', 'value' => '9000', 'compare' => '>' ) ) ),
|
||||
'Q15 hourly_rate < 50 orderby price' => $L + array( 'meta_query' => array( 'rate' => array( 'key' => 'hp_hourly_rate', 'value' => 50, 'compare' => '<', 'type' => 'NUMERIC' ), 'price' => array( 'key' => 'hp_price', 'type' => 'NUMERIC', 'compare' => 'EXISTS' ) ), 'orderby' => 'price', 'order' => 'ASC' ),
|
||||
);
|
||||
|
||||
$res = array();
|
||||
foreach ( $cases as $name => $qa ) {
|
||||
$times = array();
|
||||
$ids = null;
|
||||
$found = null;
|
||||
$err = '';
|
||||
$sql = '';
|
||||
for ( $i = 0; $i < $iters; $i++ ) {
|
||||
wp_cache_flush_group( 'tmdo_found_rows' );
|
||||
$wpdb->last_error = '';
|
||||
$t = microtime( true );
|
||||
$q = new WP_Query( $qa + array( 'fields' => 'ids', 'cache_results' => false, 'suppress_filters' => false ) );
|
||||
$times[] = ( microtime( true ) - $t ) * 1000;
|
||||
$ids = array_map( 'intval', $q->posts );
|
||||
$found = (int) $q->found_posts;
|
||||
$sql = $q->request;
|
||||
if ( $wpdb->last_error ) { $err = $wpdb->last_error; }
|
||||
}
|
||||
sort( $times );
|
||||
$res[ $name ] = array(
|
||||
'found' => $found,
|
||||
'ids' => $ids,
|
||||
'p50' => round( $times[ (int) floor( count( $times ) / 2 ) ], 2 ),
|
||||
'max' => round( max( $times ), 2 ),
|
||||
'err' => $err,
|
||||
'sql' => preg_replace( '/\s+/', ' ', $sql ),
|
||||
);
|
||||
printf( "%-36s found=%-6d p50=%8.2fms %s\n", $name, $found, $res[ $name ]['p50'], $err ? "ERR: $err" : '' );
|
||||
}
|
||||
file_put_contents( $out, wp_json_encode( array( 'mode' => TMDO_Mode_Manager::get( 'post' ), 'res' => $res ), JSON_PRETTY_PRINT ) );
|
||||
echo "saved $out (post mode=" . TMDO_Mode_Manager::get( 'post' ) . ")\n";
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
// php qdiff.php a.json b.json — compare two qbench runs (a = native reference).
|
||||
$a = json_decode( file_get_contents( $argv[1] ), true ); $b = json_decode( file_get_contents( $argv[2] ), true );
|
||||
printf( "A=%s (%s) B=%s (%s)\n", basename( $argv[1] ), $a['mode'], basename( $argv[2] ), $b['mode'] );
|
||||
printf( "%-36s %8s %8s %6s %6s %9s %9s %7s\n", 'case', 'foundA', 'foundB', 'set', 'order', 'p50A', 'p50B', 'speedup' );
|
||||
foreach ( $a['res'] as $k => $ra ) {
|
||||
$rb = $b['res'][ $k ];
|
||||
$set = ( array_diff( $ra['ids'], $rb['ids'] ) || array_diff( $rb['ids'], $ra['ids'] ) ) ? 'DIFF' : 'same';
|
||||
$ord = $ra['ids'] === $rb['ids'] ? 'same' : 'DIFF';
|
||||
printf( "%-36s %8d %8d %6s %6s %9.2f %9.2f %6.1fx %s\n", $k, $ra['found'], $rb['found'], $set, $ord, $ra['p50'], $rb['p50'], $rb['p50'] > 0 ? $ra['p50'] / $rb['p50'] : 0, $rb['err'] ? 'ERR:' . substr( $rb['err'], 0, 60 ) : '' );
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# usage: run_batch.sh <batch-tag> <scale-divisor>
|
||||
S="$(cd "$(dirname "$0")" && pwd)/seed.php"
|
||||
cd "${TMDO_LIVE_WP_PATH:-/var/www/Studio/wp-local-dev36}"
|
||||
B=$1; D=${2:-1}; OUT=${TMDO_LIVE_OUT:-/tmp}; LOG=$OUT/batch_$B.log
|
||||
run(){ wp eval-file $S "$@" 2>>$OUT/batch_${B}_err.log | grep SEED >> $LOG; }
|
||||
echo "== start $(date +%T)" >> $LOG
|
||||
run terms $((100/D)) ${B}
|
||||
for p in 1 2 3 4; do run vendors $((250/D)) ${B}p$p & done; wait
|
||||
for p in 1 2 3 4; do run listings $((1000/D)) ${B}p$p & done
|
||||
run requests $((500/D)) ${B} & run products $((1000/D)) ${B} & wait
|
||||
for p in 1 2; do run reviews $((2000/D)) ${B}p$p & done
|
||||
for p in 1 2; do run orders $((750/D)) ${B}p$p & done; wait
|
||||
echo "== end $(date +%T)" >> $LOG
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
/**
|
||||
* dev36 integration seeder — goes through the real APIs (HivePress models, WC CRUD,
|
||||
* wp_insert_user / update_*_meta) so every write passes the Hook Bus exactly like prod.
|
||||
*
|
||||
* Usage: wp eval-file seed.php <kind> <count> [tag]
|
||||
* kind: vendors | listings | requests | reviews | products | orders | terms
|
||||
*/
|
||||
|
||||
use HivePress\Models;
|
||||
|
||||
add_filter( 'pre_wp_mail', '__return_true' );
|
||||
// WC Tracks pings pixel.wp.com ~3x per product save (~180 ms each); irrelevant to the test.
|
||||
add_filter( 'pre_http_request', function ( $r, $a, $url ) { return false !== strpos( $url, 'pixel.wp.com' ) ? array( 'headers' => array(), 'body' => '', 'response' => array( 'code' => 200, 'message' => 'OK' ), 'cookies' => array() ) : $r; }, 1, 3 );
|
||||
wp_defer_term_counting( true );
|
||||
wp_defer_comment_counting( true );
|
||||
|
||||
$kind = $args[0] ?? '';
|
||||
$count = (int) ( $args[1] ?? 0 );
|
||||
$tag = $args[2] ?? 'b1';
|
||||
mt_srand( crc32( $kind . $tag ) );
|
||||
|
||||
$cities = array( 'Taipei', 'Taichung', 'Kaohsiung', 'Tainan', 'Hsinchu', 'Keelung' );
|
||||
|
||||
function s36_pick_ids( $post_type, $limit = 5000 ) {
|
||||
global $wpdb;
|
||||
return array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type=%s AND post_status='publish' ORDER BY RAND() LIMIT %d", $post_type, $limit ) ) );
|
||||
}
|
||||
|
||||
$t0 = microtime( true );
|
||||
$q0 = get_num_queries();
|
||||
$ok = 0;
|
||||
|
||||
switch ( $kind ) {
|
||||
case 'vendors':
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$login = "s36v_{$tag}_{$i}";
|
||||
$uid = wp_insert_user( array( 'user_login' => $login, 'user_email' => "$login@example.test", 'user_pass' => wp_generate_password(), 'role' => 'contributor', 'first_name' => "F$i", 'last_name' => "L$i", 'description' => "Vendor $i bio" ) );
|
||||
if ( is_wp_error( $uid ) ) { continue; }
|
||||
update_user_meta( $uid, 'billing_city', $cities[ $i % 6 ] );
|
||||
update_user_meta( $uid, 'billing_phone', '09' . mt_rand( 10000000, 99999999 ) );
|
||||
update_user_meta( $uid, 'facebook', "https://facebook.com/$login" );
|
||||
update_user_meta( $uid, 'login_count', mt_rand( 1, 300 ) );
|
||||
$v = ( new Models\Vendor() )->fill( array( 'name' => "Vendor $tag $i", 'description' => 'Seeded vendor', 'status' => 'publish', 'user' => $uid, 'verified' => (bool) mt_rand( 0, 1 ) ) );
|
||||
if ( $v->save() ) {
|
||||
update_post_meta( $v->get_id(), 'hp_hourly_rate', mt_rand( 10, 300 ) );
|
||||
update_post_meta( $v->get_id(), 'hp_rating', mt_rand( 10, 50 ) / 10 );
|
||||
$ok++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'listings':
|
||||
global $wpdb;
|
||||
$vendors = $wpdb->get_results( "SELECT ID, post_author FROM {$wpdb->posts} WHERE post_type='hp_vendor' AND post_status='publish'" );
|
||||
$cats = get_terms( array( 'taxonomy' => 'hp_listing_category', 'hide_empty' => false, 'fields' => 'ids' ) );
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$v = $vendors[ array_rand( $vendors ) ];
|
||||
$l = ( new Models\Listing() )->fill( array(
|
||||
'title' => "Listing $tag $i " . $cities[ $i % 6 ],
|
||||
'description' => str_repeat( 'Seeded listing body. ', 8 ),
|
||||
'status' => ( mt_rand( 1, 10 ) <= 9 ) ? 'publish' : 'draft',
|
||||
'user' => (int) $v->post_author,
|
||||
'vendor' => (int) $v->ID,
|
||||
'featured' => mt_rand( 1, 20 ) === 1,
|
||||
'verified' => mt_rand( 1, 3 ) === 1,
|
||||
'categories' => $cats ? array( $cats[ array_rand( $cats ) ] ) : array(),
|
||||
) );
|
||||
if ( $l->save() ) {
|
||||
$id = $l->get_id();
|
||||
update_post_meta( $id, 'hp_price', mt_rand( 100, 50000 ) );
|
||||
update_post_meta( $id, 'hp_hourly_rate', mt_rand( 10, 300 ) );
|
||||
update_post_meta( $id, 'hp_warranty', mt_rand( 0, 24 ) );
|
||||
$ok++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'requests':
|
||||
$users = get_users( array( 'fields' => 'ID', 'number' => 3000, 'orderby' => 'rand' ) );
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$r = ( new Models\Request() )->fill( array( 'title' => "Request $tag $i", 'description' => 'Need help', 'status' => 'publish', 'user' => (int) $users[ array_rand( $users ) ] ) );
|
||||
if ( $r->save() ) {
|
||||
update_post_meta( $r->get_id(), 'hp_budget', mt_rand( 500, 90000 ) );
|
||||
update_post_meta( $r->get_id(), 'hp_due_date', gmdate( 'Y-m-d', time() + mt_rand( 1, 90 ) * DAY_IN_SECONDS ) );
|
||||
$ok++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'reviews':
|
||||
$listings = s36_pick_ids( 'hp_listing' );
|
||||
$users = get_users( array( 'fields' => array( 'ID', 'display_name', 'user_email' ), 'number' => 3000, 'orderby' => 'rand' ) );
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$u = $users[ array_rand( $users ) ];
|
||||
$rv = ( new Models\Review() )->fill( array( 'text' => "Review $tag $i", 'rating' => mt_rand( 1, 5 ), 'approved' => 1, 'author' => (int) $u->ID, 'author__display_name' => $u->display_name, 'author__email' => $u->user_email, 'listing' => $listings[ array_rand( $listings ) ] ) );
|
||||
if ( $rv->save() ) { $ok++; }
|
||||
}
|
||||
break;
|
||||
|
||||
case 'terms':
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$t = wp_insert_term( "Cat $tag $i", 'hp_listing_category' );
|
||||
if ( ! is_wp_error( $t ) ) {
|
||||
update_term_meta( $t['term_id'], 'hp_sort_order', $i );
|
||||
update_term_meta( $t['term_id'], 'hp_icon', 'star' );
|
||||
$ok++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'products':
|
||||
$pcats = get_terms( array( 'taxonomy' => 'product_cat', 'hide_empty' => false, 'fields' => 'ids' ) );
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$p = new WC_Product_Simple();
|
||||
$p->set_name( "Product $tag $i" );
|
||||
$p->set_status( 'publish' );
|
||||
$reg = mt_rand( 100, 20000 );
|
||||
$p->set_regular_price( (string) $reg );
|
||||
if ( mt_rand( 1, 4 ) === 1 ) { $p->set_sale_price( (string) (int) ( $reg * 0.8 ) ); }
|
||||
$p->set_sku( "S36-$tag-$i" );
|
||||
$p->set_manage_stock( true );
|
||||
$p->set_stock_quantity( mt_rand( 0, 500 ) );
|
||||
if ( $pcats ) { $p->set_category_ids( array( $pcats[ array_rand( $pcats ) ] ) ); }
|
||||
if ( $p->save() ) { $ok++; }
|
||||
}
|
||||
break;
|
||||
|
||||
case 'orders':
|
||||
$products = wc_get_products( array( 'limit' => 2000, 'status' => 'publish', 'orderby' => 'rand', 'return' => 'ids' ) );
|
||||
$users = get_users( array( 'fields' => 'ID', 'number' => 3000, 'orderby' => 'rand' ) );
|
||||
$statuses = array( 'completed', 'completed', 'processing', 'on-hold', 'cancelled', 'refunded' );
|
||||
for ( $i = 0; $i < $count; $i++ ) {
|
||||
$o = wc_create_order( array( 'customer_id' => (int) $users[ array_rand( $users ) ] ) );
|
||||
if ( is_wp_error( $o ) ) { continue; }
|
||||
$n = mt_rand( 1, 3 );
|
||||
for ( $j = 0; $j < $n; $j++ ) { $o->add_product( wc_get_product( $products[ array_rand( $products ) ] ), mt_rand( 1, 3 ) ); }
|
||||
$o->set_billing_city( $cities[ $i % 6 ] );
|
||||
$o->set_billing_email( "buyer$i@example.test" );
|
||||
$o->calculate_totals( false );
|
||||
$o->set_status( $statuses[ array_rand( $statuses ) ] );
|
||||
$o->save();
|
||||
$ok++;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
WP_CLI::error( "unknown kind $kind" );
|
||||
}
|
||||
|
||||
wp_defer_term_counting( false );
|
||||
wp_defer_comment_counting( false );
|
||||
|
||||
$el = microtime( true ) - $t0;
|
||||
$q = get_num_queries() - $q0;
|
||||
WP_CLI::log( sprintf( 'SEED %s x%d ok=%d %.1fs %.2f ms/item %.1f queries/item mode(post=%s,user=%s,term=%s,comment=%s)',
|
||||
$kind, $count, $ok, $el, $ok ? $el * 1000 / $ok : 0, $ok ? $q / $ok : 0,
|
||||
...( class_exists( 'TMDO_Mode_Manager' ) ? array( TMDO_Mode_Manager::get( 'post' ), TMDO_Mode_Manager::get( 'user' ), TMDO_Mode_Manager::get( 'term' ), TMDO_Mode_Manager::get( 'comment' ) ) : array( 'NO-TMDO', '-', '-', '-' ) ) ) );
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* TEMPORARY test probe for the 2meet-DO integration test (2026-09-26). Delete after testing.
|
||||
* Only active when the request carries header "X-S36-Probe: 1".
|
||||
*/
|
||||
if ( empty( $_SERVER['HTTP_X_S36_PROBE'] ) ) {
|
||||
return;
|
||||
}
|
||||
add_action(
|
||||
'template_redirect',
|
||||
function () {
|
||||
global $wp_query, $wpdb;
|
||||
$ids = wp_list_pluck( (array) $wp_query->posts, 'ID' );
|
||||
header( 'X-S36-Found: ' . (int) $wp_query->found_posts );
|
||||
header( 'X-S36-Ids: ' . implode( ',', array_map( 'intval', $ids ) ) );
|
||||
header( 'X-S36-Queries: ' . (int) $wpdb->num_queries );
|
||||
header( 'X-S36-Routed: ' . ( false !== strpos( (string) $wp_query->request, 'wpdo' ) ? 1 : 0 ) );
|
||||
header( 'X-S36-Sql: ' . substr( preg_replace( '/\s+/', ' ', (string) $wp_query->request ), 0, 1500 ) );
|
||||
header( 'X-S36-Ms: ' . round( ( microtime( true ) - $_SERVER['REQUEST_TIME_FLOAT'] ) * 1000 ) );
|
||||
},
|
||||
0
|
||||
);
|
||||
Reference in New Issue
Block a user