diff --git a/.changelog/autoload-files-testability.md b/.changelog/autoload-files-testability.md new file mode 100644 index 000000000..38be71809 --- /dev/null +++ b/.changelog/autoload-files-testability.md @@ -0,0 +1,6 @@ +--- +category: fixed +type: patch +--- + +Detect PHP sources declared in Composer `autoload.files` so function-only packages run tests and coverage reporting instead of silently skipping them, without changing API documentation discovery. diff --git a/src/Project/ProjectCapabilitiesResolver.php b/src/Project/ProjectCapabilitiesResolver.php index 2481f2a68..793c318a0 100644 --- a/src/Project/ProjectCapabilitiesResolver.php +++ b/src/Project/ProjectCapabilitiesResolver.php @@ -157,7 +157,7 @@ private function resolveHasPhpSourceFiles(array $apiDirectories): bool } } - foreach (self::API_AUTOLOAD_TYPES as $autoloadType) { + foreach ([...self::API_AUTOLOAD_TYPES, 'files'] as $autoloadType) { foreach ($this->normalizeAutoloadPaths($this->composer->getAutoload($autoloadType)) as $path) { $absolutePath = $this->filesystem->getAbsolutePath($path); diff --git a/tests/Console/Command/ReportsCommandTest.php b/tests/Console/Command/ReportsCommandTest.php index 2384798a1..303059141 100644 --- a/tests/Console/Command/ReportsCommandTest.php +++ b/tests/Console/Command/ReportsCommandTest.php @@ -20,6 +20,8 @@ namespace FastForward\DevTools\Tests\Console\Command; use Symfony\Component\Console\Output\BufferedOutput; +use FastForward\DevTools\Composer\Json\ComposerJsonInterface; +use FastForward\DevTools\Filesystem\FilesystemInterface; use FastForward\DevTools\Console\Command\Traits\LogsCommandResults; use FastForward\DevTools\Console\Command\ReportsCommand; use FastForward\DevTools\Console\Output\GithubActionOutput; @@ -49,6 +51,10 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +use function Safe\file_put_contents; +use function Safe\mkdir; +use function Safe\rmdir; +use function Safe\unlink; #[UsesClass(ContainerFactory::class)] #[UsesClass(DevToolsServiceProvider::class)] @@ -253,6 +259,56 @@ public function executeWithNoCacheWillForwardNoCacheOnlyToDocsAndTests(): void self::assertSame(ReportsCommand::SUCCESS, $this->executeCommand()); } + /** + * @return void + */ + #[Test] + public function executeWillGenerateCoverageForFilesOnlyPackage(): void + { + $workspace = sys_get_temp_dir() . '/dev-tools-files-reports-' . uniqid('', true); + mkdir($workspace); + mkdir($workspace . '/tests'); + file_put_contents($workspace . '/functions.php', "prophesize(ComposerJsonInterface::class); + $filesystem = $this->prophesize(FilesystemInterface::class); + foreach (['psr-4', 'psr-0', 'classmap'] as $autoloadType) { + $composer->getAutoload($autoloadType)->willReturn([]); + } + $composer->getAutoload('files')->willReturn(['functions.php']); + $filesystem->getAbsolutePath('functions.php')->willReturn($workspace . '/functions.php'); + $filesystem->exists('./tests')->willReturn(true); + $filesystem->exists('docs')->willReturn(false); + $filesystem->exists('.github/wiki')->willReturn(false); + $resolver = new ProjectCapabilitiesResolver($composer->reveal(), $filesystem->reveal()); + $capabilities = $resolver->resolve(); + self::assertSame([], $capabilities->getApiDirectories()); + self::assertTrue($capabilities->canRunTests()); + $this->projectCapabilitiesResolver->resolve()->willReturn($capabilities); + $this->processBuilder->build([DevToolsPathResolver::getBinaryPath(), 'tests']) + ->willReturn($this->process->reveal())->shouldBeCalledOnce(); + $this->processQueue->add($this->process->reveal(), false, false, 'Generating Coverage Report') + ->shouldBeCalledOnce(); + $this->processQueue->add($this->process->reveal(), false, true, 'Generating API Docs Report') + ->shouldBeCalledOnce(); + $this->processQueue->add($this->process->reveal(), false, false, 'Generating Metrics Report') + ->shouldBeCalledOnce(); + $this->processQueue->run($this->output->reveal()) + ->willReturn(ReportsCommand::SUCCESS)->shouldBeCalledOnce(); + $this->logger->log('info', 'Generating frontpage for Fast Forward documentation...', Argument::type('array')) + ->shouldBeCalledOnce(); + $this->logger->log('info', 'Documentation reports generated successfully.', Argument::type('array')) + ->shouldBeCalledOnce(); + + self::assertSame(ReportsCommand::SUCCESS, $this->executeCommand()); + } finally { + unlink($workspace . '/functions.php'); + rmdir($workspace . '/tests'); + rmdir($workspace); + } + } + /** * @return void */ diff --git a/tests/Console/Command/TestsCommandTest.php b/tests/Console/Command/TestsCommandTest.php index d1d24a806..e397be7c9 100644 --- a/tests/Console/Command/TestsCommandTest.php +++ b/tests/Console/Command/TestsCommandTest.php @@ -36,6 +36,7 @@ use FastForward\DevTools\Path\DevToolsPathResolver; use FastForward\DevTools\Project\ProjectCapabilities; use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface; +use FastForward\DevTools\Project\ProjectCapabilitiesResolver; use FastForward\DevTools\Path\WorkingProjectPathResolver; use FastForward\DevTools\Environment\Environment as DevToolsEnvironment; use FastForward\DevTools\Environment\RuntimeEnvironment; @@ -54,6 +55,8 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Process\Process; +use function Safe\file_put_contents; +use function Safe\unlink; use function Safe\mkdir; use function Safe\rmdir; use function Safe\getcwd; @@ -69,6 +72,7 @@ #[UsesClass(ProcessBuilder::class)] #[UsesClass(ManagedWorkspace::class)] #[UsesClass(ProjectCapabilities::class)] +#[UsesClass(ProjectCapabilitiesResolver::class)] #[UsesClass(WorkingProjectPathResolver::class)] #[UsesTrait(LogsCommandResults::class)] final class TestsCommandTest extends TestCase @@ -572,6 +576,57 @@ public function executeWithInvalidMinCoverageWillReturnFailure(): void self::assertSame(TestsCommand::FAILURE, $this->invokeExecute()); } + /** + * @return void + */ + #[Test] + public function executeWillPropagateTestFailureForFilesOnlyPackage(): void + { + $workspace = sys_get_temp_dir() . '/dev-tools-files-tests-' . uniqid('', true); + mkdir($workspace); + mkdir($workspace . '/tests'); + file_put_contents($workspace . '/functions.php', "prophesize(ComposerJsonInterface::class); + $filesystem = $this->prophesize(FilesystemInterface::class); + foreach (['psr-4', 'psr-0', 'classmap'] as $autoloadType) { + $composer->getAutoload($autoloadType)->willReturn([]); + } + $composer->getAutoload('files')->willReturn(['functions.php']); + $filesystem->getAbsolutePath('functions.php')->willReturn($workspace . '/functions.php'); + $filesystem->exists('./tests')->willReturn(true); + $filesystem->exists('docs')->willReturn(false); + $filesystem->exists('.github/wiki')->willReturn(false); + $resolver = new ProjectCapabilitiesResolver($composer->reveal(), $filesystem->reveal()); + $capabilities = $resolver->resolve(); + self::assertSame([], $capabilities->getApiDirectories()); + self::assertTrue($capabilities->canRunTests()); + $this->projectCapabilitiesResolver->resolve(Argument::any())->willReturn($capabilities); + $this->processQueue->add( + Argument::that(static fn(Process $process): bool => str_contains( + $process->getCommandLine(), + DevToolsPathResolver::getPreferredToolBinaryPath('phpunit'), + )), + false, + false, + 'Running PHPUnit Tests', + )->shouldBeCalledOnce(); + $this->processQueue->run($this->output->reveal()) + ->willReturn(TestsCommand::FAILURE)->shouldBeCalledOnce(); + $this->logger->log('info', 'Running PHPUnit tests...', Argument::type('array')) + ->shouldBeCalledOnce(); + $this->logger->error('PHPUnit tests failed.', Argument::type('array')) + ->shouldBeCalledOnce(); + + self::assertSame(TestsCommand::FAILURE, $this->invokeExecute()); + } finally { + unlink($workspace . '/functions.php'); + rmdir($workspace . '/tests'); + rmdir($workspace); + } + } + /** * @return void */ diff --git a/tests/Project/ProjectCapabilitiesResolverTest.php b/tests/Project/ProjectCapabilitiesResolverTest.php index a8d025631..68e271dc1 100644 --- a/tests/Project/ProjectCapabilitiesResolverTest.php +++ b/tests/Project/ProjectCapabilitiesResolverTest.php @@ -60,6 +60,7 @@ final class ProjectCapabilitiesResolverTest extends TestCase protected function setUp(): void { $this->composer = $this->prophesize(ComposerJsonInterface::class); + $this->composer->getAutoload('files')->willReturn([]); $this->filesystem = new SymfonyFilesystem(); $this->workingDirectory = getcwd(); $this->workspace = sys_get_temp_dir() . '/dev-tools-project-capabilities-' . uniqid('', true); @@ -178,6 +179,48 @@ public function resolveWillDetectPhpSourceFromFileBasedClassmapEntries(): void self::assertFalse($capabilities->canGenerateApiDocumentation()); } + /** + * @return void + */ + #[Test] + public function resolveWillDetectFilesOnlySourceWithoutInventingApiDirectories(): void + { + mkdir($this->workspace . '/tests'); + file_put_contents($this->workspace . '/functions.php', "composer->getAutoload($type)->willReturn([]); + } + + $this->composer->getAutoload('files')->willReturn(['missing.php', 'functions.php']); + + $capabilities = $this->resolver->resolve(); + + self::assertTrue($capabilities->hasTestsPath()); + self::assertTrue($capabilities->hasPhpSourceFiles()); + self::assertTrue($capabilities->canRunTests()); + self::assertSame([], $capabilities->getApiDirectories()); + self::assertFalse($capabilities->canGenerateApiDocumentation()); + self::assertFalse($capabilities->canGenerateWiki()); + } + + /** + * @return void + */ + #[Test] + public function resolveWillIgnoreMissingAndNonPhpAutoloadFiles(): void + { + file_put_contents($this->workspace . '/README.md', 'Guide'); + + foreach (['psr-4', 'psr-0', 'classmap'] as $type) { + $this->composer->getAutoload($type)->willReturn([]); + } + + $this->composer->getAutoload('files')->willReturn(['missing.php', 'README.md']); + + self::assertFalse($this->resolver->resolve()->canRunTests()); + } + /** * @return void */