diff --git a/tests/Unit/ReportCommandTest.php b/tests/Unit/ReportCommandTest.php new file mode 100644 index 0000000..d0619d4 --- /dev/null +++ b/tests/Unit/ReportCommandTest.php @@ -0,0 +1,254 @@ +tempDir = sys_get_temp_dir().'/tactix_test_'.uniqid(); + mkdir($this->tempDir, 0777, true); + } + + protected function tearDown(): void + { + parent::tearDown(); + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + } + + private function createSerializer(): Serializer + { + return new Serializer([new ReportNormalizer(), new GetSetMethodNormalizer()], [new JsonEncoder()]); + } + + #[Test] + public function command_should_succeed_with_valid_folder(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + self::assertSame(Command::SUCCESS, $commandTester->getStatusCode()); + $output = $commandTester->getDisplay(); + self::assertStringContainsString('Report for', $output); + self::assertStringContainsString('Report written to:', $output); + } + + #[Test] + public function command_should_generate_report_files(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + $reportDir = $this->tempDir.'/report'; + self::assertFileExists($reportDir.'/index.html'); + self::assertFileExists($reportDir.'/report.js'); + self::assertFileExists($reportDir.'/chart.js'); + self::assertFileExists($reportDir.'/styles.css'); + } + + #[Test] + public function command_should_generate_valid_report_js(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + $reportJsPath = $this->tempDir.'/report/report.js'; + $content = file_get_contents($reportJsPath); + self::assertNotFalse($content, 'Failed to read report.js file'); + + self::assertStringStartsWith('const reportData = ', $content); + self::assertStringContainsString('"folder":', $content); + self::assertStringContainsString('"classes":', $content); + self::assertStringContainsString('"forbidden":', $content); + } + + #[Test] + public function command_should_generate_valid_html_structure(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + $htmlPath = $this->tempDir.'/report/index.html'; + $content = file_get_contents($htmlPath); + self::assertNotFalse($content, 'Failed to read index.html file'); + + self::assertStringContainsString('', $content); + self::assertStringContainsString('Charts Report', $content); + self::assertStringContainsString('id="pieChart"', $content); + self::assertStringContainsString('id="barChart"', $content); + self::assertStringContainsString('id="classesTableContainer"', $content); + self::assertStringContainsString('', $content); + self::assertStringContainsString('', $content); + } + + #[Test] + public function command_should_use_current_directory_when_no_out_dir_specified(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + // Change to temp directory before running command + $originalDir = getcwd(); + self::assertNotFalse($originalDir, 'Failed to get current working directory'); + chdir($this->tempDir); + + try { + $folder = __DIR__.'/../Data'; + $commandTester->execute(['folder' => $folder]); + + self::assertSame(Command::SUCCESS, $commandTester->getStatusCode()); + self::assertFileExists($this->tempDir.'/report/index.html'); + } finally { + chdir($originalDir); + } + } + + #[Test] + public function command_should_display_uncategorized_classes_when_present(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + $output = $commandTester->getDisplay(); + self::assertStringContainsString('Uncategorized Classes', $output); + } + + #[Test] + public function command_should_complete_successfully_and_show_sections(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + self::assertSame(Command::SUCCESS, $commandTester->getStatusCode()); + $output = $commandTester->getDisplay(); + // The command should always show the report title and table + self::assertStringContainsString('Report for', $output); + self::assertStringContainsString('FQCN', $output); + } + + #[Test] + public function command_should_create_output_directory_if_not_exists(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $nonExistentDir = $this->tempDir.'/nested/path/that/does/not/exist'; + self::assertDirectoryDoesNotExist($nonExistentDir); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $nonExistentDir, + ]); + + self::assertSame(Command::SUCCESS, $commandTester->getStatusCode()); + self::assertFileExists($nonExistentDir.'/report/index.html'); + } + + #[Test] + public function command_should_fail_gracefully_with_exception(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + // Use a non-existent folder that will cause an error + $folder = '/nonexistent/folder/that/does/not/exist'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + self::assertSame(Command::FAILURE, $commandTester->getStatusCode()); + $output = $commandTester->getDisplay(); + self::assertStringContainsString('[ERROR]', $output); + } + + #[Test] + public function command_should_include_class_distribution_table(): void + { + $command = new ReportCommand($this->createSerializer()); + $commandTester = new CommandTester($command); + + $folder = __DIR__.'/../Data'; + $commandTester->execute([ + 'folder' => $folder, + '--out-dir' => $this->tempDir, + ]); + + $output = $commandTester->getDisplay(); + // The table should have headers + self::assertStringContainsString('FQCN', $output); + self::assertStringContainsString('Tag', $output); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $files = scandir($dir); + if (false === $files) { + return; + } + + $files = array_diff($files, ['.', '..']); + foreach ($files as $file) { + $path = $dir.'/'.$file; + is_dir($path) ? $this->removeDirectory($path) : unlink($path); + } + rmdir($dir); + } +} diff --git a/tests/Unit/ReportNormalizer.php b/tests/Unit/ReportNormalizer.php new file mode 100644 index 0000000..ec08031 --- /dev/null +++ b/tests/Unit/ReportNormalizer.php @@ -0,0 +1,43 @@ + + */ + public function normalize(mixed $object, ?string $format = null, array $context = []): array + { + assert($object instanceof Report); + + return [ + 'aggregateRoots' => $object->aggregateRoots, + 'entities' => $object->entities, + 'factories' => $object->factories, + 'repositories' => $object->repositories, + 'services' => $object->services, + 'valueObjects' => $object->valueObjects, + 'interfaces' => $object->interfaces, + 'exceptions' => $object->exceptions, + 'uncategorized' => $object->uncategorized, + ]; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof Report; + } + + public function getSupportedTypes(?string $format): array + { + return [ + Report::class => true, + ]; + } +} diff --git a/tests/Unit/ReportTest.php b/tests/Unit/ReportTest.php new file mode 100644 index 0000000..64a1a96 --- /dev/null +++ b/tests/Unit/ReportTest.php @@ -0,0 +1,132 @@ +aggregateRoots); + self::assertSame([], $report->entities); + self::assertSame([], $report->factories); + self::assertSame([], $report->repositories); + self::assertSame([], $report->services); + self::assertSame([], $report->valueObjects); + self::assertSame([], $report->interfaces); + self::assertSame([], $report->exceptions); + self::assertSame([], $report->uncategorized); + } + + #[Test] + public function report_should_classify_interface(): void + { + $report = Report::initial()->withClassName(MyInterface::class); + + self::assertSame([MyInterface::class], $report->interfaces); + self::assertSame([], $report->entities); + self::assertSame([], $report->valueObjects); + self::assertSame([], $report->exceptions); + self::assertSame([], $report->uncategorized); + } + + #[Test] + public function report_should_classify_exception(): void + { + $report = Report::initial()->withClassName(MyException::class); + + self::assertSame([MyException::class], $report->exceptions); + self::assertSame([], $report->interfaces); + self::assertSame([], $report->entities); + self::assertSame([], $report->valueObjects); + self::assertSame([], $report->uncategorized); + } + + #[Test] + public function report_should_classify_entity(): void + { + $report = Report::initial()->withClassName(MyEntity::class); + + self::assertSame([MyEntity::class], $report->entities); + self::assertSame([], $report->interfaces); + self::assertSame([], $report->valueObjects); + self::assertSame([], $report->exceptions); + self::assertSame([], $report->uncategorized); + } + + #[Test] + public function report_should_classify_value_object(): void + { + $report = Report::initial()->withClassName(MyValueObject::class); + + self::assertSame([MyValueObject::class], $report->valueObjects); + self::assertSame([], $report->interfaces); + self::assertSame([], $report->entities); + self::assertSame([], $report->exceptions); + self::assertSame([], $report->uncategorized); + } + + #[Test] + public function report_should_classify_uncategorized(): void + { + $report = Report::initial()->withClassName(MyWithoutTag::class); + + self::assertSame([MyWithoutTag::class], $report->uncategorized); + self::assertSame([], $report->interfaces); + self::assertSame([], $report->entities); + self::assertSame([], $report->valueObjects); + self::assertSame([], $report->exceptions); + } + + #[Test] + public function report_should_accumulate_multiple_classes(): void + { + $report = Report::initial() + ->withClassName(MyInterface::class) + ->withClassName(MyEntity::class) + ->withClassName(MyValueObject::class) + ->withClassName(MyException::class) + ->withClassName(MyWithoutTag::class); + + self::assertSame([MyInterface::class], $report->interfaces); + self::assertSame([MyEntity::class], $report->entities); + self::assertSame([MyValueObject::class], $report->valueObjects); + self::assertSame([MyException::class], $report->exceptions); + self::assertSame([MyWithoutTag::class], $report->uncategorized); + } + + #[Test] + public function report_should_accumulate_multiple_classes_of_same_type(): void + { + $report = Report::initial() + ->withClassName(MyEntity::class) + ->withClassName(MyValueObject::class); + + self::assertSame([MyEntity::class], $report->entities); + self::assertSame([MyValueObject::class], $report->valueObjects); + } + + #[Test] + public function report_should_be_immutable(): void + { + $initial = Report::initial(); + $withEntity = $initial->withClassName(MyEntity::class); + + self::assertNotSame($initial, $withEntity); + self::assertSame([], $initial->entities); + self::assertSame([MyEntity::class], $withEntity->entities); + } +}