Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions src/Business/Churn/Exporter/MarkdownExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter;

use Phauthentic\CognitiveCodeAnalysis\Business\Utility\Datetime;
use Phauthentic\CognitiveCodeAnalysis\CognitiveAnalysisException;

/**
* MarkdownExporter for Churn metrics.
*/
class MarkdownExporter implements DataExporterInterface
{
/**
* @var array<string>
*/
private array $header = [
'Class',
'Score',
'Churn',
'Times Changed',
];

/**
* @var array<string>
*/
private array $headerWithCoverage = [
'Class',
'Score',
'Churn',
'Risk Churn',
'Times Changed',
'Coverage',
'Risk Level',
];

/**
* @param array<string, array<string, mixed>> $classes
* @param string $filename
* @throws CognitiveAnalysisException
*/
public function export(array $classes, string $filename): void
{
$markdown = $this->generateMarkdown($classes);

if (file_put_contents($filename, $markdown) === false) {
throw new CognitiveAnalysisException("Unable to write to file: $filename");
}
}

/**
* @param array<string, array<string, mixed>> $classes
* @return string
*/
private function generateMarkdown(array $classes): string
{
$hasCoverageData = $this->hasCoverageData($classes);
$header = $hasCoverageData ? $this->headerWithCoverage : $this->header;

$markdown = "# Churn Metrics Report\n\n";
$markdown .= "Generated: " . (new Datetime())->format('Y-m-d H:i:s') . "\n\n";
$markdown .= "Total Classes: " . count($classes) . "\n\n";

// Create table header
$markdown .= "| " . implode(" | ", $header) . " |\n";
$markdown .= "|" . str_repeat(" --- |", count($header)) . "\n";

// Add rows
foreach ($classes as $className => $data) {
if ($data['score'] == 0 || $data['churn'] == 0) {
continue;
}

$row = [
$this->escapeMarkdown($className),
(string)$data['score'],
(string)round($data['churn'], 3),
];

if ($hasCoverageData) {
$row[] = $data['riskChurn'] !== null ? (string)round($data['riskChurn'], 3) : 'N/A';
}

$row[] = (string)$data['timesChanged'];

if ($hasCoverageData) {
$row[] = $data['coverage'] !== null ? sprintf('%.2f%%', $data['coverage'] * 100) : 'N/A';
$row[] = $data['riskLevel'] ?? 'N/A';
}

$markdown .= "| " . implode(" | ", $row) . " |\n";
}

return $markdown;
}

/**
* Check if any class has coverage data
*
* @param array<string, mixed> $classes
* @return bool
*/
private function hasCoverageData(array $classes): bool
{
foreach ($classes as $data) {
if (array_key_exists('coverage', $data) && $data['coverage'] !== null) {
return true;
}
}

return false;
}

/**
* Escape special markdown characters in strings
*
* @param string $string
* @return string
*/
private function escapeMarkdown(string $string): string
{
// Escape pipe characters which would break table formatting
return str_replace('|', '\\|', $string);
}
}
1 change: 1 addition & 0 deletions src/Business/MetricsFacade.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ public function exportChurnReport(
'json' => (new Churn\Exporter\JsonExporter())->export($classes, $filename),
'csv' => (new Churn\Exporter\CsvExporter())->export($classes, $filename),
'html' => (new Churn\Exporter\HtmlExporter())->export($classes, $filename),
'markdown' => (new Churn\Exporter\MarkdownExporter())->export($classes, $filename),
'svg' => (new Churn\Exporter\SvgTreemapExporter())->export($classes, $filename),
default => null,
};
Expand Down
4 changes: 2 additions & 2 deletions src/Command/Handler/ChurnReportHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private function hasIncompleteReportOptions(?string $reportType, ?string $report

private function isValidReportType(?string $reportType): bool
{
return in_array($reportType, ['json', 'csv', 'html', 'svg-treemap'], true);
return in_array($reportType, ['json', 'csv', 'html', 'markdown', 'svg-treemap'], true);
}

/**
Expand All @@ -84,7 +84,7 @@ private function handleExceptions(Exception $exception): int
private function handleInvalidReportType(?string $reportType): int
{
$this->output->writeln(sprintf(
'<error>Invalid report type `%s` provided. Only `json`, `csv`, and `html` are accepted.</error>',
'<error>Invalid report type `%s` provided. Only `json`, `csv`, `html`, and `markdown` are accepted.</error>',
$reportType
));

Expand Down
33 changes: 33 additions & 0 deletions tests/Unit/Business/Churn/Exporter/AbstractExporterTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,37 @@ protected function getTestData(): array
],
];
}

protected function getTestDataWithCoverage(): array
{
return [
'Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics' => [
'timesChanged' => 6,
'score' => 2.042,
'file' => '/home/florian/projects/cognitive-code-checker/src/Business/Cognitive/CognitiveMetrics.php',
'churn' => 12.252,
'coverage' => 0.85,
'riskChurn' => 1.8378,
'riskLevel' => 'low',
],
'Phauthentic\CognitiveCodeAnalysis\Command\Presentation\CognitiveMetricTextRenderer' => [
'timesChanged' => 10,
'score' => 0.806,
'file' => '/home/florian/projects/cognitive-code-checker/src/Command/Presentation/CognitiveMetricTextRenderer.php',
'churn' => 8.06,
'coverage' => 0.65,
'riskChurn' => 2.821,
'riskLevel' => 'medium',
],
'Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade' => [
'timesChanged' => 8,
'score' => 0.693,
'file' => '/home/florian/projects/cognitive-code-checker/src/Business/MetricsFacade.php',
'churn' => 5.544,
'coverage' => 0.92,
'riskChurn' => 0.443,
'riskLevel' => 'low',
],
];
}
}
11 changes: 11 additions & 0 deletions tests/Unit/Business/Churn/Exporter/MarkdownExporterContent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Churn Metrics Report

Generated: 2023-10-01 12:00:00

Total Classes: 3

| Class | Score | Churn | Times Changed |
| --- | --- | --- | --- |
| Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics | 2.042 | 12.252 | 6 |
| Phauthentic\CognitiveCodeAnalysis\Command\Presentation\CognitiveMetricTextRenderer | 0.806 | 8.06 | 10 |
| Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade | 0.693 | 5.544 | 8 |
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Churn Metrics Report

Generated: 2023-10-01 12:00:00

Total Classes: 3

| Class | Score | Churn | Risk Churn | Times Changed | Coverage | Risk Level |
| --- | --- | --- | --- | --- | --- | --- |
| Phauthentic\CognitiveCodeAnalysis\Business\Cognitive\CognitiveMetrics | 2.042 | 12.252 | 1.838 | 6 | 85.00% | low |
| Phauthentic\CognitiveCodeAnalysis\Command\Presentation\CognitiveMetricTextRenderer | 0.806 | 8.06 | 2.821 | 10 | 65.00% | medium |
| Phauthentic\CognitiveCodeAnalysis\Business\MetricsFacade | 0.693 | 5.544 | 0.443 | 8 | 92.00% | low |
48 changes: 48 additions & 0 deletions tests/Unit/Business/Churn/Exporter/MarkdownExporterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace Phauthentic\CognitiveCodeAnalysis\Tests\Unit\Business\Churn\Exporter;

use Phauthentic\CognitiveCodeAnalysis\Business\Churn\Exporter\MarkdownExporter;
use PHPUnit\Framework\Attributes\Test;

/**
*
*/
class MarkdownExporterTest extends AbstractExporterTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->exporter = new MarkdownExporter();
$this->filename = sys_get_temp_dir() . '/test_metrics.md';
}

#[Test]
public function testExport(): void
{
$classes = $this->getTestData();

$this->exporter->export($classes, $this->filename);

$this->assertFileEquals(
expected: __DIR__ . '/MarkdownExporterContent.md',
actual: $this->filename
);
}

#[Test]
public function testExportWithCoverage(): void
{
$classes = $this->getTestDataWithCoverage();

$this->exporter->export($classes, $this->filename);

$this->assertFileEquals(
expected: __DIR__ . '/MarkdownExporterContentWithCoverage.md',
actual: $this->filename
);
}
}