Files
michaelschiemer/tests/Framework/Database/NPlusOneDetection/MachineLearning/NPlusOneDetectionEngineTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

401 lines
14 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Framework\Database\NPlusOneDetection\MachineLearning;
use App\Framework\Database\NPlusOneDetection\MachineLearning\NPlusOneDetectionEngine;
use App\Framework\Database\NPlusOneDetection\MachineLearning\Extractors\QueryFeatureExtractor;
use App\Framework\Database\NPlusOneDetection\QueryExecutionContext;
use App\Framework\Waf\MachineLearning\Detectors\StatisticalAnomalyDetector;
use App\Framework\MachineLearning\ValueObjects\Feature;
use App\Framework\MachineLearning\ValueObjects\FeatureType;
use App\Framework\MachineLearning\ValueObjects\AnomalyDetection;
use App\Framework\MachineLearning\ValueObjects\AnomalyType;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Percentage;
use App\Framework\DateTime\SystemClock;
use Mockery;
use Mockery\MockInterface;
// Helper function to create mock extractor
function createMockQueryExtractor(bool $enabled = true, array $features = []): MockInterface
{
$extractor = Mockery::mock(\App\Framework\MachineLearning\Core\FeatureExtractorInterface::class);
$extractor->shouldReceive('isEnabled')->andReturn($enabled);
$extractor->shouldReceive('getFeatureType')->andReturn(FeatureType::FREQUENCY);
$extractor->shouldReceive('getPriority')->andReturn(10);
$extractor->shouldReceive('canExtract')->andReturn(true);
$extractor->shouldReceive('extractFeatures')->andReturn($features);
return $extractor;
}
// Helper function to create mock detector
function createMockQueryDetector(bool $enabled = true, array $supportedTypes = [], array $anomalies = []): MockInterface
{
$supportedTypes = $supportedTypes ?: [FeatureType::FREQUENCY];
$detector = Mockery::mock(\App\Framework\MachineLearning\Core\AnomalyDetectorInterface::class);
$detector->shouldReceive('isEnabled')->andReturn($enabled);
$detector->shouldReceive('getName')->andReturn('MockDetector');
$detector->shouldReceive('getSupportedFeatureTypes')->andReturn($supportedTypes);
$detector->shouldReceive('canAnalyze')->andReturn(true);
$detector->shouldReceive('detectAnomalies')->andReturn($anomalies);
return $detector;
}
describe('NPlusOneDetectionEngine', function () {
beforeEach(function () {
$this->clock = new SystemClock();
});
afterEach(function () {
Mockery::close();
});
it('is disabled when constructed with enabled: false', function () {
$engine = new NPlusOneDetectionEngine(
enabled: false,
extractors: [],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
expect($engine->isEnabled())->toBeFalse();
});
it('is enabled when constructed with enabled: true', function () {
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
expect($engine->isEnabled())->toBeTrue();
});
it('returns configuration', function () {
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [new QueryFeatureExtractor()],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(3),
confidenceThreshold: Percentage::from(75.0)
);
$config = $engine->getConfiguration();
expect($config)->toBeArray();
expect($config['enabled'])->toBeTrue();
expect($config['extractor_count'])->toBe(1);
expect($config['detector_count'])->toBe(0);
expect($config['analysis_timeout_ms'])->toBe(3000);
expect($config['confidence_threshold'])->toBe(75.0);
});
it('returns disabled result when engine is disabled', function () {
$engine = new NPlusOneDetectionEngine(
enabled: false,
extractors: [],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->enabled)->toBeFalse();
expect($result->features)->toBeEmpty();
expect($result->anomalies)->toBeEmpty();
expect($result->confidence->getValue())->toBe(0.0);
});
it('extracts features from query context', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 10.0,
unit: 'queries/second'
);
$extractor = createMockQueryExtractor(true, [$feature]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->features)->toHaveCount(1);
expect($result->features[0]->name)->toBe('query_frequency');
});
it('detects anomalies using configured detectors', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0, // High frequency
unit: 'queries/second'
);
$anomaly = AnomalyDetection::frequencySpike(
currentRate: 100.0,
baseline: 10.0,
threshold: 3.0
);
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->anomalies)->not->toBeEmpty();
expect($result->confidence->getValue())->toBeGreaterThan(0.0);
});
it('filters anomalies by confidence threshold', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 50.0,
unit: 'queries/second'
);
// Low confidence anomaly (should be filtered out)
$lowConfidenceAnomaly = AnomalyDetection::create(
type: AnomalyType::FREQUENCY_SPIKE,
featureType: FeatureType::FREQUENCY,
anomalyScore: 0.5, // 50% confidence
description: 'Low confidence anomaly',
features: [$feature],
evidence: []
);
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$lowConfidenceAnomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(75.0) // High threshold
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Anomaly should be filtered out due to low confidence
expect($result->anomalies)->toBeEmpty();
});
it('skips disabled extractors', function () {
$enabledExtractor = createMockQueryExtractor(true, [
new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
]);
$disabledExtractor = createMockQueryExtractor(false, [
new Feature(FeatureType::FREQUENCY, 'feature2', 20.0, 'unit')
]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$enabledExtractor, $disabledExtractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Should only have features from enabled extractor
expect($result->features)->toHaveCount(1);
expect($result->features[0]->name)->toBe('feature1');
});
it('skips disabled detectors', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0,
unit: 'queries/second'
);
$anomaly = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0);
$extractor = createMockQueryExtractor(true, [$feature]);
$enabledDetector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
$disabledDetector = createMockQueryDetector(false, [FeatureType::FREQUENCY], [$anomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$enabledDetector, $disabledDetector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Should only have anomalies from enabled detector
expect($result->detectorResults)->toHaveCount(1);
});
it('includes extractor results in output', function () {
$extractor = createMockQueryExtractor(true, [
new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->extractorResults)->not->toBeEmpty();
expect($result->extractorResults[0]->featuresExtracted)->toBe(1);
expect($result->extractorResults[0]->success)->toBeTrue();
});
it('includes detector results in output', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0,
unit: 'queries/second'
);
$anomaly = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0);
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
expect($result->detectorResults)->not->toBeEmpty();
expect($result->detectorResults[0]->anomaliesDetected)->toBe(1);
expect($result->detectorResults[0]->success)->toBeTrue();
});
it('calculates overall confidence from anomalies', function () {
$feature = new Feature(
type: FeatureType::FREQUENCY,
name: 'query_frequency',
value: 100.0,
unit: 'queries/second'
);
$anomaly1 = AnomalyDetection::frequencySpike(100.0, 10.0, 3.0); // ~95% confidence
$anomaly2 = AnomalyDetection::frequencySpike(90.0, 10.0, 3.0); // ~95% confidence
$extractor = createMockQueryExtractor(true, [$feature]);
$detector = createMockQueryDetector(true, [FeatureType::FREQUENCY], [$anomaly1, $anomaly2]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [$detector],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Overall confidence should be average of anomalies
expect($result->confidence->getValue())->toBeGreaterThan(90.0);
});
it('handles exceptions gracefully', function () {
$extractor = Mockery::mock(\App\Framework\MachineLearning\Core\FeatureExtractorInterface::class);
$extractor->shouldReceive('isEnabled')->andReturn(true);
$extractor->shouldReceive('canExtract')->andReturn(true);
$extractor->shouldReceive('extractFeatures')->andThrow(new \RuntimeException('Extraction failed'));
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Should not throw, should return error in result
expect($result->error)->not->toBeNull();
expect($result->error)->toContain('Extraction failed');
});
it('tracks analysis time', function () {
$extractor = createMockQueryExtractor(true, [
new Feature(FeatureType::FREQUENCY, 'feature1', 10.0, 'unit')
]);
$engine = new NPlusOneDetectionEngine(
enabled: true,
extractors: [$extractor],
detectors: [],
clock: $this->clock,
analysisTimeout: Duration::fromSeconds(5),
confidenceThreshold: Percentage::from(60.0)
);
$context = QueryExecutionContext::minimal();
$result = $engine->analyzeQueryContext($context);
// Analysis time should be measured
expect($result->analysisTime->toMilliseconds())->toBeGreaterThanOrEqual(0);
});
});