- 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
290 lines
12 KiB
PHP
290 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Framework\Discovery;
|
|
|
|
use App\Framework\Cache\Cache;
|
|
use App\Framework\Cache\CacheItem;
|
|
use App\Framework\Cache\Driver\InMemoryCache;
|
|
use App\Framework\Cache\GeneralCache;
|
|
use App\Framework\Console\ConsoleCommand;
|
|
use App\Framework\Context\ExecutionContext;
|
|
use App\Framework\Core\PathProvider;
|
|
use App\Framework\DateTime\Clock;
|
|
use App\Framework\DateTime\SystemClock;
|
|
use App\Framework\DI\Container;
|
|
use App\Framework\DI\DefaultContainer;
|
|
use App\Framework\Discovery\Cache\DiscoveryCacheIdentifiers;
|
|
use App\Framework\Discovery\DiscoveryServiceBootstrapper;
|
|
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
|
use App\Framework\Serializer\Php\PhpSerializer;
|
|
use App\Framework\Serializer\Php\PhpSerializerConfig;
|
|
|
|
describe('DiscoveryService Integration with Console Commands', function () {
|
|
beforeEach(function () {
|
|
// Create fresh container and dependencies for each test
|
|
$this->container = new DefaultContainer();
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
// Use correct base path for Docker environment
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$this->pathProvider = new PathProvider($basePath);
|
|
|
|
// Register dependencies in container
|
|
$this->container->singleton(Cache::class, $this->cache);
|
|
$this->container->singleton(Clock::class, $this->clock);
|
|
$this->container->singleton(PathProvider::class, $this->pathProvider);
|
|
|
|
// Clear cache between tests
|
|
$this->cache->clear();
|
|
|
|
// Clear any opcache
|
|
if (function_exists('opcache_reset')) {
|
|
opcache_reset();
|
|
}
|
|
});
|
|
|
|
it('properly handles different execution contexts in discovery', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Test console context discovery
|
|
$consoleContext = ExecutionContext::forConsole();
|
|
$paths = [$this->pathProvider->getSourcePath()];
|
|
|
|
// Perform discovery
|
|
$registry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null);
|
|
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($registry->isEmpty())->toBeFalse();
|
|
|
|
// Should have discovered ConsoleCommand attributes
|
|
$consoleCommands = $registry->attributes->get(ConsoleCommand::class);
|
|
expect($consoleCommands)->not->toBeEmpty();
|
|
});
|
|
|
|
it('finds the correct number of console commands after fresh discovery', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Perform fresh discovery
|
|
$registry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null);
|
|
|
|
// Get console commands
|
|
$consoleCommands = $registry->attributes->get(ConsoleCommand::class);
|
|
|
|
// Should find a reasonable number of commands (at least the demo commands)
|
|
expect(count($consoleCommands))->toBeGreaterThanOrEqual(6); // Demo commands
|
|
expect(count($consoleCommands))->toBeLessThan(100); // Reasonable upper bound
|
|
|
|
// Should include demo commands
|
|
$commandNames = [];
|
|
foreach ($consoleCommands as $discovered) {
|
|
$command = $discovered->createAttributeInstance();
|
|
$commandNames[] = $command->name;
|
|
}
|
|
|
|
expect($commandNames)->toContain('demo:hello');
|
|
expect($commandNames)->toContain('demo:colors');
|
|
expect($commandNames)->toContain('demo:interactive');
|
|
});
|
|
|
|
it('caches discovery results properly', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->cache);
|
|
|
|
// First discovery should cache results
|
|
$registry1 = $bootstrapper->bootstrap();
|
|
|
|
// Check that cache contains discovery results
|
|
$paths = [$this->pathProvider->getSourcePath()];
|
|
$context = ExecutionContext::detect();
|
|
$cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value);
|
|
|
|
$cachedItem = $this->cache->get($cacheKey);
|
|
expect($cachedItem->isHit)->toBeTrue();
|
|
|
|
// Second discovery should use cache
|
|
$registry2 = $bootstrapper->bootstrap();
|
|
|
|
// Results should be equivalent
|
|
expect($registry1->isEmpty())->toBe($registry2->isEmpty());
|
|
|
|
$commands1 = $registry1->attributes->get(ConsoleCommand::class);
|
|
$commands2 = $registry2->attributes->get(ConsoleCommand::class);
|
|
expect(count($commands1))->toBe(count($commands2));
|
|
});
|
|
|
|
it('handles cache misses gracefully with fallback discovery', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Ensure cache is empty
|
|
$this->cache->clear();
|
|
|
|
// Bootstrap should perform fresh discovery when cache is empty
|
|
$registry = $bootstrapper->bootstrap();
|
|
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($registry->isEmpty())->toBeFalse();
|
|
|
|
// Should have found console commands even without cache
|
|
$consoleCommands = $registry->attributes->get(ConsoleCommand::class);
|
|
expect($consoleCommands)->not->toBeEmpty();
|
|
});
|
|
|
|
it('separates cache by execution context', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
$paths = [$this->pathProvider->getSourcePath()];
|
|
|
|
// Create cache keys for different contexts
|
|
$consoleKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'console');
|
|
$cliScriptKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, 'cli-script');
|
|
|
|
// Keys should be different
|
|
expect($consoleKey->toString())->not->toBe($cliScriptKey->toString());
|
|
|
|
// Cache data for console context
|
|
$consoleRegistry = $bootstrapper->performBootstrap($this->pathProvider, $this->cache, null);
|
|
|
|
// Check that only console cache is populated
|
|
$consoleCached = $this->cache->get($consoleKey);
|
|
$cliScriptCached = $this->cache->get($cliScriptKey);
|
|
|
|
expect($consoleCached->isHit)->toBeTrue();
|
|
expect($cliScriptCached->isHit)->toBeFalse();
|
|
});
|
|
|
|
it('handles invalid cached data gracefully', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
$paths = [$this->pathProvider->getSourcePath()];
|
|
$context = ExecutionContext::detect();
|
|
|
|
// Put invalid data in cache
|
|
$cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($paths, $context->getType()->value);
|
|
$invalidItem = CacheItem::forSetting($cacheKey, 'invalid_data');
|
|
$this->cache->set($invalidItem);
|
|
|
|
// Bootstrap should handle invalid cache gracefully and perform fresh discovery
|
|
$registry = $bootstrapper->bootstrap();
|
|
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($registry->isEmpty())->toBeFalse();
|
|
});
|
|
|
|
it('processes initializers after discovery', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Bootstrap should process initializers
|
|
$registry = $bootstrapper->bootstrap();
|
|
|
|
// Registry should be registered in container
|
|
expect($this->container->has(DiscoveryRegistry::class))->toBeTrue();
|
|
$containerRegistry = $this->container->get(DiscoveryRegistry::class);
|
|
expect($containerRegistry)->toBe($registry);
|
|
});
|
|
|
|
it('detects when discovery is required', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Initially should require discovery
|
|
expect($bootstrapper->isDiscoveryRequired())->toBeTrue();
|
|
|
|
// After bootstrap should still work
|
|
$bootstrapper->bootstrap();
|
|
|
|
// The method should still be callable
|
|
$isRequired = $bootstrapper->isDiscoveryRequired();
|
|
expect($isRequired)->toBeOfType('boolean');
|
|
});
|
|
|
|
it('performs incremental discovery when service is available', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// First bootstrap
|
|
$registry1 = $bootstrapper->bootstrap();
|
|
|
|
// Incremental bootstrap should work
|
|
$registry2 = $bootstrapper->incrementalBootstrap();
|
|
|
|
expect($registry2)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
|
|
// Should have registered new registry in container
|
|
expect($this->container->has(DiscoveryRegistry::class))->toBeTrue();
|
|
});
|
|
|
|
it('falls back to full bootstrap when incremental is not possible', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Incremental without previous bootstrap should fall back to full bootstrap
|
|
$registry = $bootstrapper->incrementalBootstrap();
|
|
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($registry->isEmpty())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('DiscoveryService Performance and Error Handling', function () {
|
|
beforeEach(function () {
|
|
$this->container = new DefaultContainer();
|
|
$cacheDriver = new InMemoryCache();
|
|
$serializer = new PhpSerializer(PhpSerializerConfig::safe());
|
|
$this->cache = new GeneralCache($cacheDriver, $serializer);
|
|
$this->clock = new SystemClock();
|
|
// Use correct base path for Docker environment
|
|
$basePath = file_exists('/var/www/html/src') ? '/var/www/html' : '/home/michael/dev/michaelschiemer';
|
|
$this->pathProvider = new PathProvider($basePath);
|
|
|
|
$this->container->singleton(Cache::class, $this->cache);
|
|
$this->container->singleton(Clock::class, $this->clock);
|
|
$this->container->singleton(PathProvider::class, $this->pathProvider);
|
|
});
|
|
|
|
it('completes discovery within reasonable time', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
$startTime = microtime(true);
|
|
$registry = $bootstrapper->bootstrap();
|
|
$endTime = microtime(true);
|
|
|
|
$duration = $endTime - $startTime;
|
|
|
|
// Discovery should complete within 30 seconds (generous for CI)
|
|
expect($duration)->toBeLessThan(30.0);
|
|
|
|
// Should still produce valid results
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
expect($registry->isEmpty())->toBeFalse();
|
|
});
|
|
|
|
it('handles discovery errors gracefully', function () {
|
|
// Create bootstrapper with invalid path
|
|
$invalidPathProvider = new PathProvider('/nonexistent/path');
|
|
$this->container->instance(PathProvider::class, $invalidPathProvider);
|
|
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
// Should not throw exception, but may produce empty or partial results
|
|
$registry = $bootstrapper->bootstrap();
|
|
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
// May be empty due to invalid path, but should not crash
|
|
});
|
|
|
|
it('maintains memory usage within reasonable bounds', function () {
|
|
$bootstrapper = new DiscoveryServiceBootstrapper($this->container, $this->clock);
|
|
|
|
$initialMemory = memory_get_usage(true);
|
|
|
|
$registry = $bootstrapper->bootstrap();
|
|
|
|
$finalMemory = memory_get_usage(true);
|
|
$memoryIncrease = $finalMemory - $initialMemory;
|
|
|
|
// Memory increase should be reasonable (less than 50MB)
|
|
expect($memoryIncrease)->toBeLessThan(50 * 1024 * 1024);
|
|
|
|
// Should still produce valid results
|
|
expect($registry)->toBeInstanceOf(DiscoveryRegistry::class);
|
|
});
|
|
});
|