diff --git a/Makefile b/Makefile index 7d87c61..0c4d570 100644 --- a/Makefile +++ b/Makefile @@ -192,6 +192,10 @@ e2e-customer-profile-local: ## Teste la fiche client dédiée (dépôt réel + s e2e-pilotage-schema-local: ## Teste le schéma du pilotage (migration idempotente, nullabilité, contraintes uniques, sauvegarde/restauration) — copies temporaires, rien de réel touché $(DC) run --rm wpcli wp eval "require ABSPATH . '$(THEME)/tools/e2e-pilotage-schema-local.php';" --user=admin +.PHONY: e2e-pilotage-perf-local +e2e-pilotage-perf-local: ## Teste la performance du registre des recettes sur un historique volumineux (20 000 lignes) — copie temporaire, index vérifié, rien de réel touché + $(DC) run --rm wpcli wp eval "require ABSPATH . '$(THEME)/tools/e2e-pilotage-perf-local.php';" --user=admin + .PHONY: e2e-address-lookup-local e2e-address-lookup-local: ## Teste l'autocomplétion d'adresse (BAN, appels interceptés, endpoint câblé), rien d'écrit $(DC) run --rm wpcli wp eval "require ABSPATH . '$(THEME)/tools/e2e-address-lookup-local.php';" --user=admin diff --git a/www/wp-content/themes/luziapi/tools/e2e-pilotage-perf-local.php b/www/wp-content/themes/luziapi/tools/e2e-pilotage-perf-local.php new file mode 100644 index 0000000..220846d --- /dev/null +++ b/www/wp-content/themes/luziapi/tools/e2e-pilotage-perf-local.php @@ -0,0 +1,31 @@ + ! $r['ok'])); +if ($failed > 0) { + WP_CLI::error($failed . ' assertion(s) en échec.'); +} +WP_CLI::success(count($run['results']) . ' assertions OK — performance du registre validée.'); diff --git a/www/wp-content/themes/luziapi/tools/e2e-pilotage-perf.php b/www/wp-content/themes/luziapi/tools/e2e-pilotage-perf.php new file mode 100644 index 0000000..c6f0ef3 --- /dev/null +++ b/www/wp-content/themes/luziapi/tools/e2e-pilotage-perf.php @@ -0,0 +1,98 @@ +, cleanup: string, fatal: ?string} + */ + function luziapi_e2e_pilotage_perf_run(): array + { + global $wpdb; + $results = []; + $assert = static function (string $label, bool $ok, string $detail = '') use (&$results): void { + $results[] = ['label' => $label, 'ok' => $ok, 'detail' => $detail]; + }; + $fatal = null; + $cleanup = 'non exécuté'; + $temp = ''; + + $schema = new \LuziApi\Shop\Infrastructure\WordPress\PilotageSchemaManager($wpdb); + + try { + $schema->migrate(); + $real = $schema->tableName(); + $temp = $real . '_e2e_perf_tmp'; + $wpdb->query("CREATE TEMPORARY TABLE {$temp} LIKE {$real}"); + + // 20 000 lignes : 100 dans la fenêtre cible (juin 2099), 19 900 de bruit + // répartis sur les autres mois (jamais juin) — la fenêtre est donc très + // sélective. + $targetRows = 100; + $noiseRows = 19_900; + $noiseMonths = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12]; + + $values = []; + for ($i = 0; $i < $noiseRows; ++$i) { + $date = sprintf('2099-%02d-%02d 12:00:00', $noiseMonths[$i % 11], ($i % 28) + 1); + $values[] = "('{$date}', 100, 'cash', 'collection', 'noise', 1, '{$date}')"; + } + for ($i = 0; $i < $targetRows; ++$i) { + $values[] = "('2099-06-15 12:00:00', 100, 'cash', 'collection', 'target', 1, '2099-06-15 12:00:00')"; + } + shuffle($values); + + $columns = '(occurred_at, amount_cents, payment_method, entry_type, description, created_by, created_at)'; + foreach (array_chunk($values, 2_000) as $chunk) { + $wpdb->query("INSERT INTO {$temp} {$columns} VALUES " . implode(',', $chunk)); + } + $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$temp}"); + $assert('Historique volumineux inséré (20 000 lignes)', 20_000 === $total, "lignes={$total}"); + + $window = "occurred_at >= '2099-06-15 00:00:00' AND occurred_at <= '2099-06-15 23:59:59'"; + $registerQuery = "SELECT * FROM {$temp} WHERE {$window} ORDER BY occurred_at DESC, sequence_number DESC"; + + // Correction à l'échelle. + $start = microtime(true); + $rows = $wpdb->get_results($registerQuery, ARRAY_A); + $elapsed = microtime(true) - $start; + $sum = array_sum(array_map(static fn (array $r): int => (int) $r['amount_cents'], $rows)); + $assert('Registre à l’échelle : exactement les 100 lignes de la fenêtre', 100 === count($rows), 'lignes=' . count($rows)); + $assert('Registre à l’échelle : somme correcte (100 × 1,00 € = 100 €)', 10_000 === $sum, "cents={$sum}"); + + // Plan d'exécution : l'index occurred_at est utilisé (pas de full scan). + $plan = $wpdb->get_results("EXPLAIN SELECT * FROM {$temp} WHERE {$window}", ARRAY_A); + $planType = strtoupper((string) ($plan[0]['type'] ?? '')); + $planKey = (string) ($plan[0]['key'] ?? ''); + $assert('Plan d’exécution : pas de balayage complet (index occurred_at utilisé)', 'ALL' !== $planType && str_contains($planKey, 'occurred_at'), "type={$planType} key={$planKey}"); + + // Garde-fou de temps (généreux) contre une régression catastrophique. + $assert('Temps de requête raisonnable (< 1 s sur 20 000 lignes)', $elapsed < 1.0, sprintf('%.3f s', $elapsed)); + + $cleanup = 'ok (table temporaire uniquement, aucune donnée réelle touchée)'; + } catch (\Throwable $exception) { + $fatal = $exception->getMessage(); + } finally { + if ('' !== $temp) { + $wpdb->query("DROP TEMPORARY TABLE IF EXISTS {$temp}"); + } + } + + return ['results' => $results, 'cleanup' => $cleanup, 'fatal' => $fatal]; + } +}