diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 05652e425..d9f5664b9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,9 @@ + + ./tests/ContainerFactoryTest.php + ./tests/IntegrationTests.php diff --git a/tests/ContainerFactoryTest.php b/tests/ContainerFactoryTest.php new file mode 100644 index 000000000..298e7527d --- /dev/null +++ b/tests/ContainerFactoryTest.php @@ -0,0 +1,310 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Cecil\Test; + +use Cecil\Builder; +use Cecil\Cache; +use Cecil\Config; +use Cecil\Container\ContainerFactory; +use Cecil\Converter\Converter; +use Cecil\Converter\Parsedown; +use Cecil\Logger\PrintLogger; +use Cecil\Renderer\Twig; +use Cecil\Renderer\Twig\TwigFactory; +use Cecil\Util; +use DI\Container; +use DI\NotFoundException; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Symfony\Component\Filesystem\Filesystem; + +/** + * Tests for ContainerFactory and dependency injection functionality. + * + * This test class verifies: + * 1. ContainerFactory successfully creates a container with all registered services + * 2. Services can be resolved from the container + * 3. Attribute-based injection works correctly + * 4. The fallback mechanism in Builder::build() works as expected + * 5. Cache instances are properly created via Builder::getCache() + */ +class ContainerFactoryTest extends TestCase +{ + protected Builder $builder; + protected Container $container; + protected string $source; + /** + * Set to true to keep the generated files after the test. + * This is useful for debugging purposes, but should not be used in CI. + */ + public const DEBUG = false; + + public function setUp(): void + { + // Use existing test fixtures to create Builder with a real Config + $this->source = Util::joinFile(__DIR__, 'fixtures/website'); + $configFile = Util::joinFile($this->source, 'config.yml'); + + if (!file_exists($configFile)) { + $this->markTestSkipped('Test fixtures not available'); + return; + } + + $logger = new PrintLogger(Builder::VERBOSITY_NORMAL); + $this->builder = Builder::create(Config::loadFile($configFile), $logger); + $this->container = $this->builder->getContainer(); + } + + public function tearDown(): void + { + $fs = new Filesystem(); + if (!self::DEBUG) { + $fs->remove(Util::joinFile($this->source, '.cecil')); + $fs->remove(Util::joinFile($this->source, '.cache')); + $fs->remove(Util::joinFile($this->source, '_site')); + } + } + + /** + * Test 1: ContainerFactory successfully creates a container with all registered services. + */ + public function testContainerFactoryCreatesContainer(): void + { + $this->assertInstanceOf(Container::class, $this->container); + } + + /** + * Test 2: Verify Config and Logger are properly injected into the container. + */ + public function testContainerHasConfigAndLogger(): void + { + // Config should be resolvable + $config = $this->container->get(Config::class); + $this->assertInstanceOf(Config::class, $config); + + // Logger should be resolvable + $logger = $this->container->get(LoggerInterface::class); + $this->assertInstanceOf(LoggerInterface::class, $logger); + } + + /** + * Test 3: Services can be resolved from the container - Steps. + */ + public function testContainerResolvesSteps(): void + { + // Test a sample of step classes + $stepsToTest = [ + \Cecil\Step\Pages\Load::class, + \Cecil\Step\Data\Load::class, + \Cecil\Step\Pages\Create::class, + \Cecil\Step\Pages\Convert::class, + ]; + + foreach ($stepsToTest as $stepClass) { + $this->assertTrue( + $this->container->has($stepClass), + "Container should have {$stepClass}" + ); + + // Note: Steps are not fully instantiated here because they require Builder + // as a constructor parameter. Builder is injected after container creation, + // so we verify the definitions exist without triggering instantiation. + } + } + + /** + * Test 4: Services can be resolved from the container - Generators. + */ + public function testContainerResolvesGenerators(): void + { + // Test a sample of generator classes + $generatorsToTest = [ + \Cecil\Generator\Homepage::class, + \Cecil\Generator\Section::class, + \Cecil\Generator\Taxonomy::class, + \Cecil\Generator\Pagination::class, + ]; + + foreach ($generatorsToTest as $generatorClass) { + $this->assertTrue( + $this->container->has($generatorClass), + "Container should have {$generatorClass}" + ); + } + } + + /** + * Test 5: Verify TwigFactory can be resolved and used. + */ + public function testContainerResolvesTwigFactory(): void + { + $this->assertTrue($this->container->has(TwigFactory::class)); + + // Note: Full instantiation would require Builder, but we can verify + // the container knows about the factory + } + + /** + * Test 6: Verify Builder is properly registered in the container. + * The Builder injects itself into the container after creation. + */ + public function testBuilderIsInContainer(): void + { + // Verify Builder itself is in the container + $this->assertTrue($this->container->has(Builder::class)); + $builderFromContainer = $this->container->get(Builder::class); + $this->assertSame($this->builder, $builderFromContainer); + } + + /** + * Test 7: Verify converter services can be resolved with dependencies. + */ + public function testContainerResolvesConverterServices(): void + { + $this->assertTrue($this->container->has(Parsedown::class)); + $this->assertTrue($this->container->has(Converter::class)); + + // Note: These services depend on Builder (Parsedown requires Builder via Config parameter). + // Since Builder injects itself after container creation (see ContainerFactory::create), + // we verify definitions exist without instantiation to avoid initialization order issues. + } + + /** + * Test 8: Test fallback mechanism simulation. + * While we can't easily test the actual Builder::build() fallback without + * modifying the container state, we can verify NotFoundException behavior. + */ + public function testContainerThrowsNotFoundExceptionForUnknownService(): void + { + $this->expectException(NotFoundException::class); + + // Try to get a service that doesn't exist + $this->container->get('NonExistentService'); + } + + /** + * Test 9: Test Builder::getCache() method. + * This verifies cache instances are properly created. + */ + public function testBuilderGetCacheMethod(): void + { + // Test cache creation with default pool + $cache1 = $this->builder->getCache(); + $this->assertInstanceOf(Cache::class, $cache1); + + // Test cache creation with named pool + $cache2 = $this->builder->getCache('test-pool'); + $this->assertInstanceOf(Cache::class, $cache2); + + // Verify different pools create different instances + $this->assertNotSame($cache1, $cache2); + + // Verify same pool called twice creates new instances each time + $cache3 = $this->builder->getCache('test-pool'); + $this->assertInstanceOf(Cache::class, $cache3); + $this->assertNotSame($cache2, $cache3); + } + + /** + * Test 10: Verify container compiles in production mode. + */ + public function testContainerCompilationInProduction(): void + { + // Create a new builder without debug mode + $source = Util::joinFile(__DIR__, 'fixtures/website'); + $configFile = Util::joinFile($source, 'config.yml'); + $logger = new PrintLogger(Builder::VERBOSITY_NORMAL); + + $builder = Builder::create(Config::loadFile($configFile), $logger); + $container = $builder->getContainer(); + + $this->assertInstanceOf(Container::class, $container); + + // The container should work even with compilation enabled + $this->assertTrue($container->has(Config::class)); + } + + /** + * Test 11: Verify container works in debug mode. + */ + public function testContainerInDebugMode(): void + { + // Save original value to restore later + $originalValue = getenv('CECIL_DEBUG'); + + // Set debug environment variable + putenv('CECIL_DEBUG=true'); + + try { + $source = Util::joinFile(__DIR__, 'fixtures/website'); + $configFile = Util::joinFile($source, 'config.yml'); + $logger = new PrintLogger(Builder::VERBOSITY_NORMAL); + + $builder = Builder::create(Config::loadFile($configFile), $logger); + $container = $builder->getContainer(); + + $this->assertInstanceOf(Container::class, $container); + + // The container should work without compilation in debug mode + $this->assertTrue($container->has(Config::class)); + } finally { + // Restore original environment variable value + if ($originalValue !== false) { + putenv("CECIL_DEBUG={$originalValue}"); + } else { + putenv('CECIL_DEBUG=false'); + } + } + } + + /** + * Test 12: Test the complete build process with DI container. + * This is an integration test that verifies the DI container works + * throughout the entire build lifecycle. + */ + public function testFullBuildWithDependencyInjection(): void + { + $source = Util::joinFile(__DIR__, 'fixtures/website'); + + $this->builder->setSourceDir($source); + $this->builder->setDestinationDir($source); + + // Build the site - this exercises the fallback mechanism in Builder::build() + // If build() completes without throwing an exception, the test passes + $this->builder->build([ + 'drafts' => false, + 'dry-run' => true, // Use dry-run to avoid writing files + ]); + } + + /** + * Test 13: Verify lazy loading of services. + */ + public function testLazyLoadedServices(): void + { + // Core extension is marked as lazy + $this->assertTrue($this->container->has(\Cecil\Renderer\Extension\Core::class)); + + // Note: We can't fully test lazy loading without triggering instantiation, + // but we can verify the definition exists + } + + /** + * Test 14: Verify factory definitions work correctly. + */ + public function testFactoryDefinitions(): void + { + // Twig and Cache use factory definitions + $this->assertTrue($this->container->has(Twig::class)); + $this->assertTrue($this->container->has(Cache::class)); + } +}