Skip to content

Commit 1e16e22

Browse files
Merge pull request #6 from floriankraemer/add-csv-report
Add CSV report generation and associated tests
2 parents bf48ba4 + 6b9b5bc commit 1e16e22

4 files changed

Lines changed: 400 additions & 1 deletion

File tree

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/// <copyright company="Florian Krämer">
2+
/// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
/// </copyright>
4+
5+
using System.Text;
6+
7+
using CognitiveCodeAnalysis.CognitiveAnalysis;
8+
using CognitiveCodeAnalysis.CognitiveAnalysis.Reports;
9+
using CognitiveCodeAnalysis.Configuration;
10+
using CognitiveCodeAnalysis.HalsteadAnalysis;
11+
12+
namespace CognitiveCodeAnalysis.Tests.CognitiveAnalysis.Reports;
13+
14+
public class CsvReportTests
15+
{
16+
private static string WriteCsvAndRead(CognitiveMetricsCollection coll, CognitiveConfiguration config)
17+
{
18+
var path = Path.Combine(Path.GetTempPath(), "cog-csv-" + Guid.NewGuid() + ".csv");
19+
try
20+
{
21+
new CsvReport().RenderMetrics(path, coll, config);
22+
return File.ReadAllText(path);
23+
}
24+
finally
25+
{
26+
if (File.Exists(path))
27+
{
28+
File.Delete(path);
29+
}
30+
}
31+
}
32+
33+
[Test]
34+
public void CsvReport_WritesHeaderAndDataRow()
35+
{
36+
var m = SampleMetric(totalScore: 12.0, line: 10);
37+
var coll = new CognitiveMetricsCollection { m };
38+
var config = new CognitiveConfiguration { ScoreThreshold = 5.0, ShowOnlyMethodsExceedingThreshold = false };
39+
40+
var csv = WriteCsvAndRead(coll, config);
41+
var lines = csv.TrimEnd().Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
42+
43+
Assert.That(lines.Length, Is.EqualTo(2));
44+
Assert.That(lines[0], Does.StartWith("FilePath,ClassName,MethodName,MethodSignature,LineNumber,TotalScore"));
45+
Assert.That(lines[1], Does.Contain("/tmp/Sample.cs"));
46+
Assert.That(lines[1], Does.Contain(",C,"));
47+
Assert.That(lines[1], Does.Contain(",Foo,"));
48+
Assert.That(lines[1], Does.Contain(",10,"));
49+
Assert.That(lines[1], Does.Contain(",12.000,"));
50+
}
51+
52+
[Test]
53+
public void CsvReport_RespectsThresholdFilter()
54+
{
55+
var low = SampleMetric(totalScore: 1.0, line: 1);
56+
low.MethodName = "Low";
57+
var high = SampleMetric(totalScore: 10.0, line: 2);
58+
high.MethodName = "High";
59+
var coll = new CognitiveMetricsCollection { low, high };
60+
var config = new CognitiveConfiguration { ScoreThreshold = 5.0, ShowOnlyMethodsExceedingThreshold = true };
61+
62+
var csv = WriteCsvAndRead(coll, config);
63+
var lines = csv.TrimEnd().Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
64+
65+
Assert.That(lines.Length, Is.EqualTo(2)); // header + 1 data
66+
Assert.That(lines[1], Does.Contain("High"));
67+
Assert.That(lines[1], Does.Not.Contain("Low"));
68+
}
69+
70+
[Test]
71+
public void CsvReport_EscapesSpecialCharacters()
72+
{
73+
var m = new CognitiveMetrics(
74+
methodName: "Bar\"Baz",
75+
className: "My,Class",
76+
filePath: "/tmp/Sample.cs",
77+
methodSignature: "void Bar()",
78+
methodLineNumber: 5
79+
);
80+
m.totalScore = 3.0;
81+
var coll = new CognitiveMetricsCollection { m };
82+
var config = new CognitiveConfiguration { ScoreThreshold = 0.0, ShowOnlyMethodsExceedingThreshold = false };
83+
84+
var csv = WriteCsvAndRead(coll, config);
85+
var rows = ParseCsv(csv);
86+
87+
Assert.That(rows.Count, Is.EqualTo(2));
88+
// columns: 0=FilePath, 1=ClassName, 2=MethodName
89+
Assert.That(rows[1][1], Is.EqualTo("My,Class"));
90+
Assert.That(rows[1][2], Is.EqualTo("Bar\"Baz"));
91+
}
92+
93+
[Test]
94+
public void CsvReport_IncludesHalsteadWhenEnabled()
95+
{
96+
var m = SampleMetric(totalScore: 4.0, line: 1);
97+
m.Halstead = new HalsteadMetrics { Volume = 123.45, Difficulty = 2.5, Effort = 308.625 };
98+
var coll = new CognitiveMetricsCollection { m };
99+
var configEnabled = new CognitiveConfiguration { ScoreThreshold = 0.0, ShowOnlyMethodsExceedingThreshold = false, ShowHalsteadComplexity = true };
100+
var configDisabled = new CognitiveConfiguration { ScoreThreshold = 0.0, ShowOnlyMethodsExceedingThreshold = false, ShowHalsteadComplexity = false };
101+
102+
var csvEnabled = WriteCsvAndRead(coll, configEnabled);
103+
var headerEnabled = csvEnabled.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)[0];
104+
Assert.That(headerEnabled, Does.Contain("HalsteadVolume"));
105+
Assert.That(headerEnabled, Does.Contain("HalsteadDifficulty"));
106+
Assert.That(headerEnabled, Does.Contain("HalsteadEffort"));
107+
108+
var csvDisabled = WriteCsvAndRead(coll, configDisabled);
109+
var headerDisabled = csvDisabled.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)[0];
110+
Assert.That(headerDisabled, Does.Not.Contain("HalsteadVolume"));
111+
}
112+
113+
[Test]
114+
public void CsvReport_IncludesCoverageColumnsWhenPresent()
115+
{
116+
var withCov = new CognitiveMetrics(
117+
methodName: "Cov",
118+
className: "C",
119+
filePath: "/tmp/Cov.cs",
120+
methodSignature: "void Cov()",
121+
methodLineNumber: 1,
122+
lineCoveragePercentage: 82.3,
123+
branchCoveragePercentage: 55.0
124+
);
125+
withCov.totalScore = 1.0;
126+
withCov.churnScore = 0.42;
127+
128+
var withoutCov = SampleMetric(totalScore: 2.0, line: 2);
129+
130+
var collWith = new CognitiveMetricsCollection { withCov };
131+
var collWithout = new CognitiveMetricsCollection { withoutCov };
132+
var config = new CognitiveConfiguration { ScoreThreshold = 0.0, ShowOnlyMethodsExceedingThreshold = false };
133+
134+
var csvWith = WriteCsvAndRead(collWith, config);
135+
var headerWith = csvWith.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)[0];
136+
Assert.That(headerWith, Does.Contain("LineCoveragePercent"));
137+
Assert.That(headerWith, Does.Contain("BranchCoveragePercent"));
138+
Assert.That(headerWith, Does.Contain("ChurnScore"));
139+
140+
var csvWithout = WriteCsvAndRead(collWithout, config);
141+
var headerWithout = csvWithout.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)[0];
142+
Assert.That(headerWithout, Does.Not.Contain("LineCoveragePercent"));
143+
}
144+
145+
private static CognitiveMetrics SampleMetric(double totalScore, int line)
146+
{
147+
var m = new CognitiveMetrics(
148+
methodName: "Foo",
149+
className: "C",
150+
filePath: "/tmp/Sample.cs",
151+
methodSignature: "void Foo()",
152+
methodLineNumber: line
153+
);
154+
m.totalScore = totalScore;
155+
return m;
156+
}
157+
158+
private static List<List<string>> ParseCsv(string content)
159+
{
160+
var result = new List<List<string>>();
161+
if (string.IsNullOrEmpty(content))
162+
{
163+
return result;
164+
}
165+
166+
var normalized = content.Replace("\r\n", "\n").Replace("\r", "\n");
167+
var lines = normalized.Split('\n');
168+
169+
foreach (var line in lines)
170+
{
171+
if (string.IsNullOrEmpty(line) && result.Count > 0)
172+
{
173+
continue;
174+
}
175+
176+
result.Add(ParseCsvLine(line));
177+
}
178+
179+
if (result.Count > 0 && result[^1].All(string.IsNullOrEmpty))
180+
{
181+
result.RemoveAt(result.Count - 1);
182+
}
183+
184+
return result;
185+
}
186+
187+
private static List<string> ParseCsvLine(string line)
188+
{
189+
var fields = new List<string>();
190+
var field = new StringBuilder();
191+
bool inQuote = false;
192+
193+
for (int i = 0; i < line.Length; i++)
194+
{
195+
char c = line[i];
196+
if (inQuote)
197+
{
198+
if (c == '"')
199+
{
200+
if (i + 1 < line.Length && line[i + 1] == '"')
201+
{
202+
field.Append('"');
203+
i++;
204+
}
205+
else
206+
{
207+
inQuote = false;
208+
}
209+
}
210+
else
211+
{
212+
field.Append(c);
213+
}
214+
}
215+
else if (c == '"')
216+
{
217+
inQuote = true;
218+
}
219+
else if (c == ',')
220+
{
221+
fields.Add(field.ToString());
222+
field.Clear();
223+
}
224+
else
225+
{
226+
field.Append(c);
227+
}
228+
}
229+
230+
fields.Add(field.ToString());
231+
return fields;
232+
}
233+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/// <copyright company="Florian Krämer">
2+
/// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
/// </copyright>
4+
5+
using System.Globalization;
6+
using System.Text;
7+
8+
using CognitiveCodeAnalysis.CognitiveAnalysis;
9+
using CognitiveCodeAnalysis.Configuration;
10+
11+
namespace CognitiveCodeAnalysis.CognitiveAnalysis.Reports;
12+
13+
public sealed class CsvReport : IReport
14+
{
15+
public string Name => "Csv";
16+
17+
public void RenderMetrics(
18+
string outputFile,
19+
CognitiveMetricsCollection metricsCollection,
20+
CognitiveConfiguration configuration
21+
)
22+
{
23+
var filtered = ReportMetricsFilter.FilterForReport(metricsCollection, configuration);
24+
bool hasCoverageData = filtered.HasCoverageData();
25+
26+
var sb = new StringBuilder();
27+
var headers = BuildHeaders(hasCoverageData, configuration);
28+
sb.AppendLine(string.Join(",", headers.Select(EscapeCsvField)));
29+
30+
foreach (var m in filtered.OrderByDescending(m => m.totalScore))
31+
{
32+
var row = BuildRow(m, hasCoverageData, configuration);
33+
sb.AppendLine(string.Join(",", row.Select(EscapeCsvField)));
34+
}
35+
36+
CognitiveReportFileWriter.Write(outputFile, sb.ToString());
37+
}
38+
39+
private static List<string> BuildHeaders(bool hasCoverageData, CognitiveConfiguration configuration)
40+
{
41+
var headers = new List<string>
42+
{
43+
"FilePath",
44+
"ClassName",
45+
"MethodName",
46+
"MethodSignature",
47+
"LineNumber",
48+
"TotalScore",
49+
"LinesOfCode",
50+
"IfCount",
51+
"IfScore",
52+
"ArgumentCount",
53+
"ArgumentScore",
54+
"NestingLevels",
55+
"NestingScore",
56+
"ReturnCount",
57+
"ReturnScore",
58+
"LocalVariableCount",
59+
"LocalVariableScore",
60+
"FieldAccessCount",
61+
"FieldAccessScore",
62+
"PropertyAccessCount",
63+
"PropertyAccessScore",
64+
};
65+
66+
if (configuration.ShowHalsteadComplexity)
67+
{
68+
headers.Add("HalsteadVolume");
69+
headers.Add("HalsteadDifficulty");
70+
headers.Add("HalsteadEffort");
71+
}
72+
73+
if (configuration.ShowCyclomaticComplexity)
74+
{
75+
headers.Add("CyclomaticComplexity");
76+
}
77+
78+
if (hasCoverageData)
79+
{
80+
headers.Add("LineCoveragePercent");
81+
headers.Add("BranchCoveragePercent");
82+
headers.Add("ChurnScore");
83+
}
84+
85+
return headers;
86+
}
87+
88+
private static List<string> BuildRow(
89+
CognitiveMetrics m,
90+
bool hasCoverageData,
91+
CognitiveConfiguration configuration
92+
)
93+
{
94+
var row = new List<string>
95+
{
96+
m.FilePath,
97+
m.ClassName,
98+
m.MethodName,
99+
m.methodSignature,
100+
m.methodLineNumber.ToString(CultureInfo.InvariantCulture),
101+
m.totalScore.ToString("F3", CultureInfo.InvariantCulture),
102+
m.linesOfCode.ToString(CultureInfo.InvariantCulture),
103+
m.ifCount.ToString(CultureInfo.InvariantCulture),
104+
m.ifScore.ToString("F3", CultureInfo.InvariantCulture),
105+
m.argumentCount.ToString(CultureInfo.InvariantCulture),
106+
m.argumentScore.ToString("F3", CultureInfo.InvariantCulture),
107+
m.nestingLevels.ToString(CultureInfo.InvariantCulture),
108+
m.nestingScore.ToString("F3", CultureInfo.InvariantCulture),
109+
m.returnCount.ToString(CultureInfo.InvariantCulture),
110+
m.returnScore.ToString("F3", CultureInfo.InvariantCulture),
111+
m.localVariableCount.ToString(CultureInfo.InvariantCulture),
112+
m.localVariableScore.ToString("F3", CultureInfo.InvariantCulture),
113+
m.fieldAccessCount.ToString(CultureInfo.InvariantCulture),
114+
m.fieldAccessScore.ToString("F3", CultureInfo.InvariantCulture),
115+
m.propertyAccessCount.ToString(CultureInfo.InvariantCulture),
116+
m.propertyAccessScore.ToString("F3", CultureInfo.InvariantCulture),
117+
};
118+
119+
if (configuration.ShowHalsteadComplexity)
120+
{
121+
row.Add(FormatHalstead(m.Halstead?.Volume));
122+
row.Add(FormatHalstead(m.Halstead?.Difficulty));
123+
row.Add(FormatHalstead(m.Halstead?.Effort));
124+
}
125+
126+
if (configuration.ShowCyclomaticComplexity)
127+
{
128+
row.Add(m.cyclomaticComplexity.ToString("F1", CultureInfo.InvariantCulture));
129+
}
130+
131+
if (hasCoverageData)
132+
{
133+
row.Add(FormatCoverage(m.lineCoveragePercentage));
134+
row.Add(FormatCoverage(m.branchCoveragePercentage));
135+
row.Add(FormatChurn(m.churnScore));
136+
}
137+
138+
return row;
139+
}
140+
141+
private static string FormatHalstead(double? value)
142+
=> value.HasValue ? value.Value.ToString("F2", CultureInfo.InvariantCulture) : "";
143+
144+
private static string FormatCoverage(double? value)
145+
=> value.HasValue ? value.Value.ToString("F1", CultureInfo.InvariantCulture) : "";
146+
147+
private static string FormatChurn(double? value)
148+
=> value.HasValue ? value.Value.ToString("F3", CultureInfo.InvariantCulture) : "";
149+
150+
private static string EscapeCsvField(string? value)
151+
{
152+
if (string.IsNullOrEmpty(value))
153+
{
154+
return "";
155+
}
156+
157+
bool needsQuoting = value.Contains(',') || value.Contains('"') || value.Contains('\r') || value.Contains('\n');
158+
if (needsQuoting)
159+
{
160+
return "\"" + value.Replace("\"", "\"\"") + "\"";
161+
}
162+
163+
return value;
164+
}
165+
}

0 commit comments

Comments
 (0)