Files
michaelschiemer/tests/Framework/Performance/PerformanceMetricsIntegrationTest.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

608 lines
18 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Byte;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Database\ConnectionInterface;
use App\Framework\Database\Migration\Migration;
use App\Framework\Database\Migration\MigrationCollection;
use App\Framework\Database\Migration\MigrationRunner;
use App\Framework\Database\Migration\MigrationVersion;
use App\Framework\Database\Platform\MySQLPlatform;
use App\Framework\Database\QueryBuilder\QueryBuilder;
use App\Framework\Database\QueryBuilder\QueryBuilderFactory;
use App\Framework\Database\ResultInterface;
use App\Framework\Database\ValueObjects\SqlQuery;
use App\Framework\DateTime\SystemClock;
use App\Framework\Performance\Entity\PerformanceMetric;
use App\Framework\Performance\MemoryMonitor;
use App\Framework\Performance\OperationTracker;
use App\Framework\Performance\PerformanceCategory;
use App\Framework\Performance\Repository\PerformanceMetricsRepository;
describe('Performance Metrics Integration', function () {
beforeEach(function () {
$this->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();
}
}