connection = new PerformanceMetricsMockConnection(); $this->platform = new MySQLPlatform(); $this->clock = new SystemClock(); $this->memoryMonitor = new MemoryMonitor(); $this->operationTracker = new OperationTracker($this->clock, $this->memoryMonitor); $this->queryBuilderFactory = new PerformanceMetricsMockQueryBuilderFactory(); $this->performanceMetricsRepository = new PerformanceMetricsRepository( $this->connection, $this->queryBuilderFactory ); $this->migrationRunner = new MigrationRunner( $this->connection, $this->platform, $this->clock, null, // tableConfig null, // logger $this->operationTracker, $this->memoryMonitor, null, // performanceReporter null, // memoryThresholds $this->performanceMetricsRepository ); }); test('migration runner persists performance metrics for successful migrations', function () { $migration = new PerformanceTestMigration(); $migrations = new MigrationCollection($migration); // Set no applied migrations initially $this->connection->setAppliedMigrations([]); $result = $this->migrationRunner->migrate($migrations); expect($result)->toContain('2024_01_01_000000'); expect($migration->wasExecuted())->toBeTrue(); // Verify performance metrics were persisted $savedMetrics = $this->queryBuilderFactory->getInsertedData('performance_metrics'); expect($savedMetrics)->toHaveCount(1); $metric = $savedMetrics[0]; expect($metric['operation_type'])->toBe('migration_execution'); expect($metric['category'])->toBe('DATABASE'); expect($metric['migration_version'])->toBe('2024_01_01_000000'); expect($metric['success'])->toBe(true); expect($metric['error_message'])->toBeNull(); expect($metric['metadata'])->toContain('migration_description'); }); test('migration runner persists performance metrics for failed migrations', function () { $migration = new PerformanceMetricsFailingTestMigration(); $migrations = new MigrationCollection($migration); // Set no applied migrations initially $this->connection->setAppliedMigrations([]); expect(function () { $this->migrationRunner->migrate($migrations); })->toThrow(Exception::class); // Verify failed migration performance metrics were persisted $savedMetrics = $this->queryBuilderFactory->getInsertedData('performance_metrics'); expect($savedMetrics)->toHaveCount(1); $metric = $savedMetrics[0]; expect($metric['operation_type'])->toBe('migration_execution'); expect($metric['category'])->toBe('DATABASE'); expect($metric['migration_version'])->toBe('2024_01_01_000001'); expect($metric['success'])->toBe(false); expect($metric['error_message'])->toContain('Test migration failure'); expect($metric['metadata'])->toContain('error_type'); }); test('migration runner persists performance metrics for rollbacks', function () { $migration = new PerformanceTestMigration(); $migrations = new MigrationCollection($migration); // Set migration as already applied $this->connection->setAppliedMigrations([ ['version' => '2024_01_01_000000', 'description' => 'Performance Test Migration'], ]); $result = $this->migrationRunner->rollback($migrations, 1); expect($result)->toContain($migration); // Verify rollback performance metrics were persisted $savedMetrics = $this->queryBuilderFactory->getInsertedData('performance_metrics'); expect($savedMetrics)->toHaveCount(1); $metric = $savedMetrics[0]; expect($metric['operation_type'])->toBe('rollback_execution'); expect($metric['category'])->toBe('DATABASE'); expect($metric['migration_version'])->toBe('2024_01_01_000000'); expect($metric['success'])->toBe(true); expect($metric['metadata'])->toContain('rollback_steps'); }); test('performance metrics repository can query metrics by migration version', function () { // Create test metrics $version = MigrationVersion::fromTimestamp('2024_01_01_000000'); $metrics = [ PerformanceMetric::fromPerformanceSnapshot( 'migration_test_1', 'migration_execution', PerformanceCategory::DATABASE, Duration::fromMilliseconds(1500), Byte::fromBytes(50 * 1024 * 1024), Byte::fromBytes(55 * 1024 * 1024), Byte::fromBytes(60 * 1024 * 1024), Byte::fromBytes(5 * 1024 * 1024), true, null, $version, ['test' => 'data'] ), PerformanceMetric::fromPerformanceSnapshot( 'rollback_test_1', 'rollback_execution', PerformanceCategory::DATABASE, Duration::fromMilliseconds(800), Byte::fromBytes(55 * 1024 * 1024), Byte::fromBytes(50 * 1024 * 1024), Byte::fromBytes(55 * 1024 * 1024), Byte::fromBytes(-5 * 1024 * 1024), true, null, $version, ['rollback' => 'data'] ), ]; // Save metrics $this->performanceMetricsRepository->saveBatch($metrics); // Query by migration version $foundMetrics = $this->performanceMetricsRepository->findByMigrationVersion($version); expect($foundMetrics)->toHaveCount(2); expect($foundMetrics[0]->operationType)->toBeIn(['migration_execution', 'rollback_execution']); expect($foundMetrics[1]->operationType)->toBeIn(['migration_execution', 'rollback_execution']); }); test('performance metrics repository can get performance statistics', function () { $since = new DateTimeImmutable('-1 hour'); // Create test metrics with different categories $metrics = [ PerformanceMetric::fromPerformanceSnapshot( 'migration_1', 'migration_execution', PerformanceCategory::DATABASE, Duration::fromMilliseconds(1000), Byte::fromBytes(50 * 1024 * 1024), Byte::fromBytes(55 * 1024 * 1024), Byte::fromBytes(60 * 1024 * 1024), Byte::fromBytes(5 * 1024 * 1024), true ), PerformanceMetric::fromPerformanceSnapshot( 'migration_2', 'migration_execution', PerformanceCategory::DATABASE, Duration::fromMilliseconds(2000), Byte::fromBytes(60 * 1024 * 1024), Byte::fromBytes(65 * 1024 * 1024), Byte::fromBytes(70 * 1024 * 1024), Byte::fromBytes(5 * 1024 * 1024), false, 'Test failure' ), ]; $this->performanceMetricsRepository->saveBatch($metrics); $statistics = $this->performanceMetricsRepository->getPerformanceStatistics($since); expect($statistics)->toHaveCount(1); // Only DATABASE category expect($statistics[0]['category'])->toBe(PerformanceCategory::DATABASE); expect($statistics[0]['total_operations'])->toBe(2); expect($statistics[0]['successful_operations'])->toBe(1); expect($statistics[0]['success_rate'])->toBe(0.5); expect($statistics[0]['avg_execution_time'])->toBeInstanceOf(Duration::class); }); test('performance metrics repository can find slow operations', function () { $slowThreshold = Duration::fromMilliseconds(1500); $metrics = [ PerformanceMetric::fromPerformanceSnapshot( 'fast_migration', 'migration_execution', PerformanceCategory::DATABASE, Duration::fromMilliseconds(500), // Fast Byte::fromBytes(50 * 1024 * 1024), Byte::fromBytes(55 * 1024 * 1024), Byte::fromBytes(60 * 1024 * 1024), Byte::fromBytes(5 * 1024 * 1024), true ), PerformanceMetric::fromPerformanceSnapshot( 'slow_migration', 'migration_execution', PerformanceCategory::DATABASE, Duration::fromMilliseconds(3000), // Slow Byte::fromBytes(60 * 1024 * 1024), Byte::fromBytes(70 * 1024 * 1024), Byte::fromBytes(75 * 1024 * 1024), Byte::fromBytes(10 * 1024 * 1024), true ), ]; $this->performanceMetricsRepository->saveBatch($metrics); $slowOperations = $this->performanceMetricsRepository->findSlowOperations($slowThreshold); expect($slowOperations)->toHaveCount(1); expect($slowOperations[0]->operationId)->toBe('slow_migration'); expect($slowOperations[0]->executionTime->toMilliseconds())->toBe(3000); }); }); // Test fixtures class PerformanceTestMigration implements Migration { private bool $executed = false; public function up(ConnectionInterface $connection): void { $this->executed = true; // Simulate migration execution $connection->execute(SqlQuery::create('CREATE TABLE performance_test (id INT)')); } public function down(ConnectionInterface $connection): void { $connection->execute(SqlQuery::create('DROP TABLE performance_test')); } public function getDescription(): string { return 'Performance Test Migration'; } public function getVersion(): MigrationVersion { return MigrationVersion::fromTimestamp('2024_01_01_000000'); } public function wasExecuted(): bool { return $this->executed; } } class PerformanceMetricsFailingTestMigration implements Migration { public function up(ConnectionInterface $connection): void { throw new \Exception('Test migration failure'); } public function down(ConnectionInterface $connection): void { // Empty } public function getDescription(): string { return 'Failing Test Migration'; } public function getVersion(): MigrationVersion { return MigrationVersion::fromTimestamp('2024_01_01_000001'); } } class PerformanceMetricsMockConnection implements ConnectionInterface { private array $queries = []; private array $appliedMigrations = []; private bool $inTransaction = false; public function setAppliedMigrations(array $migrations): void { $this->appliedMigrations = $migrations; } public function execute(SqlQuery $query): int { $this->queries[] = ['type' => 'execute', 'sql' => $query->sql, 'params' => $query->parameters->toArray()]; return 1; } public function query(SqlQuery $query): ResultInterface { $this->queries[] = ['type' => 'query', 'sql' => $query->sql, 'params' => $query->parameters->toArray()]; return new PerformanceMetricsMockResult($this->appliedMigrations); } public function queryOne(SqlQuery $query): ?array { return null; } public function queryColumn(SqlQuery $query): array { $this->queries[] = ['type' => 'queryColumn', 'sql' => $query->sql, 'params' => $query->parameters->toArray()]; return array_column($this->appliedMigrations, 'version'); } public function queryScalar(SqlQuery $query): mixed { $this->queries[] = ['type' => 'queryScalar', 'sql' => $query->sql, 'params' => $query->parameters->toArray()]; // Return '1' for connectivity test (SELECT 1) if (str_contains($query->sql, 'SELECT 1')) { return '1'; } return null; } public function beginTransaction(): void { $this->inTransaction = true; } public function commit(): void { $this->inTransaction = false; } public function rollback(): void { $this->inTransaction = false; } public function inTransaction(): bool { return $this->inTransaction; } public function lastInsertId(): string { return '1'; } public function getPdo(): \PDO { return new class () extends \PDO { public function __construct() { // Skip parent constructor to avoid actual DB connection } public function getAttribute(int $attribute): mixed { return match($attribute) { \PDO::ATTR_DRIVER_NAME => 'mysql', default => null }; } }; } public function getQueries(): array { return $this->queries; } } class PerformanceMetricsMockResult implements ResultInterface { private array $data; public function __construct(array $data) { $this->data = $data; } public function fetch(): ?array { return $this->data[0] ?? null; } public function fetchAll(): array { return $this->data; } public function fetchOne(): ?array { return $this->data[0] ?? null; } public function fetchColumn(int $column = 0): array { return array_column($this->data, $column); } public function fetchScalar(): mixed { $row = $this->fetchOne(); return $row ? array_values($row)[0] : null; } public function rowCount(): int { return count($this->data); } public function getIterator(): \Traversable { return new \ArrayIterator($this->data); } public function count(): int { return count($this->data); } } class PerformanceMetricsMockQueryBuilderFactory { private array $insertedData = []; public function create(ConnectionInterface $connection): QueryBuilder { return new PerformanceMetricsMockQueryBuilder($this); } public function recordInsert(string $table, array $data): void { if (! isset($this->insertedData[$table])) { $this->insertedData[$table] = []; } $this->insertedData[$table][] = $data; } public function getInsertedData(string $table): array { return $this->insertedData[$table] ?? []; } } class PerformanceMetricsMockQueryBuilder implements QueryBuilder { private string $tableName = ''; private array $wheres = []; private string $orderBy = ''; private ?int $limitValue = null; public function __construct(private PerformanceMetricsMockQueryBuilderFactory $factory) { } public function table(string $table): self { $this->tableName = $table; return $this; } public function select(array $columns): self { return $this; } public function where(string $column, string $operator, mixed $value): self { $this->wheres[] = [$column, $operator, $value]; return $this; } public function orderBy(string $column, string $direction = 'ASC'): self { $this->orderBy = "$column $direction"; return $this; } public function limit(int $limit): self { $this->limitValue = $limit; return $this; } public function groupBy(string $column): self { return $this; } public function get(): array { // Mock return based on table and conditions if ($this->tableName === 'performance_metrics') { return $this->factory->getInsertedData('performance_metrics'); } return []; } public function first(): ?array { $results = $this->get(); return $results[0] ?? null; } public function count(): int { return count($this->get()); } public function average(string $column): ?float { $results = $this->get(); if (empty($results)) { return null; } $values = array_column($results, $column); return array_sum($values) / count($values); } public function insert(array $data): int { $this->factory->recordInsert($this->tableName, $data); return 1; // Mock insert ID } public function insertBatch(array $data): int { foreach ($data as $row) { $this->factory->recordInsert($this->tableName, $row); } return count($data); } public function update(array $data): int { return 1; } public function delete(): int { return 1; } public function toSql(): string { return "SELECT * FROM {$this->tableName}"; } public function getParameters(): array { return []; } public function execute(): mixed { return $this->get(); } }