Skip to content

Commit af2a5cc

Browse files
committed
Merge branch '641-cobertura-crap-score' of https://github.com/rikrak/ReportGenerator
2 parents cd908be + 9dfe2e9 commit af2a5cc

File tree

5 files changed

+152
-50
lines changed

5 files changed

+152
-50
lines changed

src/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ bin
1616
obj
1717
.DS_Store
1818
.AssemblyAttributes
19-
node_modules
19+
node_modules
20+
/_ReSharper.Caches

src/Readme.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ For further details take a look at LICENSE.txt.
6767

6868
CHANGELOG
6969

70+
5.2.1.0
71+
72+
* New: Added 'Crap Score' metric for Coberatura coverage files (contrbuted by @rikrak)
73+
7074
5.2.0.0
7175

7276
* New: Added support for .NET 8. Dropped support for .NET 3.1 and .NET 5.0

src/ReportGenerator.Console.NetCore/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"ReportGenerator.Console.NetCore": {
44
"commandName": "Project",
5-
"commandLineArgs": "\"-reports:..\\..\\..\\..\\..\\Testprojects\\CSharp\\Reports\\OpenCover.xml\" \"-targetdir:C:\\Users\\Daniel Palme\\Desktop\\coverage\" -reporttypes:Html;"
5+
"commandLineArgs": "\"-reports:..\\..\\..\\..\\..\\src\\Testprojects\\CSharp\\Reports\\Cobertura_coverlet.xml\" \"-targetdir:C:\\Users\\Daniel Palme\\Desktop\\coverage2\" -reporttypes:Html;"
66
}
77
}
88
}

src/ReportGenerator.Core.Test/Parser/CoberturaParserTest.cs

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,17 @@ namespace Palmmedia.ReportGenerator.Core.Test.Parser
1919
[Collection("FileManager")]
2020
public class CoberturaParserTest
2121
{
22-
private static readonly string FilePath1 = Path.Combine(FileManager.GetJavaReportDirectory(), "Cobertura2.1.1.xml");
22+
private static readonly string FilePathJavaReport = Path.Combine(FileManager.GetJavaReportDirectory(), "Cobertura2.1.1.xml");
23+
private static readonly string FilePathCSharpReport = Path.Combine(FileManager.GetCSharpReportDirectory(), "Cobertura_coverlet.xml");
2324

24-
private readonly ParserResult parserResult;
25+
private readonly ParserResult javaParserResult;
26+
private readonly ParserResult csharpParserResult;
2527

2628
public CoberturaParserTest()
2729
{
28-
var filterMock = new Mock<IFilter>();
29-
filterMock.Setup(f => f.IsElementIncludedInReport(It.IsAny<string>())).Returns(true);
30+
this.javaParserResult = ParseReport(FilePathJavaReport);
3031

31-
var report = XDocument.Load(FilePath1);
32-
new CoberturaReportPreprocessor().Execute(report);
33-
this.parserResult = new CoberturaParser(filterMock.Object, filterMock.Object, filterMock.Object).Parse(report.Root);
32+
this.csharpParserResult = ParseReport(FilePathCSharpReport);
3433
}
3534

3635
/// <summary>
@@ -39,7 +38,7 @@ public CoberturaParserTest()
3938
[Fact]
4039
public void SupportsBranchCoverage()
4140
{
42-
Assert.True(this.parserResult.SupportsBranchCoverage);
41+
Assert.True(this.javaParserResult.SupportsBranchCoverage);
4342
}
4443

4544
/// <summary>
@@ -48,7 +47,7 @@ public void SupportsBranchCoverage()
4847
[Fact]
4948
public void NumberOfLineVisitsTest()
5049
{
51-
var fileAnalysis = GetFileAnalysis(this.parserResult.Assemblies, "test.TestClass", "C:\\temp\\test\\TestClass.java");
50+
var fileAnalysis = GetFileAnalysis(this.javaParserResult.Assemblies, "test.TestClass", "C:\\temp\\test\\TestClass.java");
5251
Assert.Equal(1, fileAnalysis.Lines.Single(l => l.LineNumber == 15).LineVisits);
5352
Assert.Equal(1, fileAnalysis.Lines.Single(l => l.LineNumber == 17).LineVisits);
5453
Assert.Equal(0, fileAnalysis.Lines.Single(l => l.LineNumber == 20).LineVisits);
@@ -61,7 +60,7 @@ public void NumberOfLineVisitsTest()
6160
[Fact]
6261
public void LineVisitStatusTest()
6362
{
64-
var fileAnalysis = GetFileAnalysis(this.parserResult.Assemblies, "test.TestClass", "C:\\temp\\test\\TestClass.java");
63+
var fileAnalysis = GetFileAnalysis(this.javaParserResult.Assemblies, "test.TestClass", "C:\\temp\\test\\TestClass.java");
6564

6665
var line = fileAnalysis.Lines.Single(l => l.LineNumber == 1);
6766
Assert.Equal(LineVisitStatus.NotCoverable, line.LineVisitStatus);
@@ -82,7 +81,7 @@ public void LineVisitStatusTest()
8281
[Fact]
8382
public void NumberOfFilesTest()
8483
{
85-
Assert.Equal(7, this.parserResult.Assemblies.SelectMany(a => a.Classes).SelectMany(a => a.Files).Distinct().Count());
84+
Assert.Equal(7, this.javaParserResult.Assemblies.SelectMany(a => a.Classes).SelectMany(a => a.Files).Distinct().Count());
8685
}
8786

8887
/// <summary>
@@ -91,8 +90,8 @@ public void NumberOfFilesTest()
9190
[Fact]
9291
public void FilesOfClassTest()
9392
{
94-
Assert.Single(this.parserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.TestClass").Files);
95-
Assert.Single(this.parserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.GenericClass").Files);
93+
Assert.Single(this.javaParserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.TestClass").Files);
94+
Assert.Single(this.javaParserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.GenericClass").Files);
9695
}
9796

9897
/// <summary>
@@ -101,7 +100,7 @@ public void FilesOfClassTest()
101100
[Fact]
102101
public void ClassesInAssemblyTest()
103102
{
104-
Assert.Equal(7, this.parserResult.Assemblies.SelectMany(a => a.Classes).Count());
103+
Assert.Equal(7, this.javaParserResult.Assemblies.SelectMany(a => a.Classes).Count());
105104
}
106105

107106
/// <summary>
@@ -110,7 +109,7 @@ public void ClassesInAssemblyTest()
110109
[Fact]
111110
public void AssembliesTest()
112111
{
113-
Assert.Equal(2, this.parserResult.Assemblies.Count);
112+
Assert.Equal(2, this.javaParserResult.Assemblies.Count);
114113
}
115114

116115
/// <summary>
@@ -119,7 +118,7 @@ public void AssembliesTest()
119118
[Fact]
120119
public void GetCoverableLinesOfClassTest()
121120
{
122-
Assert.Equal(3, this.parserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.AbstractClass").CoverableLines);
121+
Assert.Equal(3, this.javaParserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.AbstractClass").CoverableLines);
123122
}
124123

125124
/// <summary>
@@ -128,18 +127,73 @@ public void GetCoverableLinesOfClassTest()
128127
[Fact]
129128
public void MethodMetricsTest()
130129
{
131-
var metrics = this.parserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.TestClass").Files.Single(f => f.Path == "C:\\temp\\test\\TestClass.java").MethodMetrics;
130+
var metrics = this.javaParserResult.Assemblies.Single(a => a.Name == "test").Classes.Single(c => c.Name == "test.TestClass").Files.Single(f => f.Path == "C:\\temp\\test\\TestClass.java").MethodMetrics.ToArray();
132131

133132
Assert.Equal(4, metrics.Count());
134-
Assert.Equal("<init>()V", metrics.First().FullName);
135-
Assert.Equal(3, metrics.First().Metrics.Count());
136-
137-
Assert.Equal("Cyclomatic complexity", metrics.First().Metrics.ElementAt(0).Name);
138-
Assert.Equal(0, metrics.First().Metrics.ElementAt(0).Value);
139-
Assert.Equal("Line coverage", metrics.First().Metrics.ElementAt(1).Name);
140-
Assert.Equal(100.0M, metrics.First().Metrics.ElementAt(1).Value);
141-
Assert.Equal("Branch coverage", metrics.First().Metrics.ElementAt(2).Name);
142-
Assert.Equal(100.0M, metrics.First().Metrics.ElementAt(2).Value);
133+
134+
var initMethodMetric = metrics.First();
135+
Assert.Equal("<init>()V", initMethodMetric.FullName);
136+
Assert.Equal(4, initMethodMetric.Metrics.Count());
137+
138+
var complexityMetric = initMethodMetric.Metrics.Single(m => m.MetricType == MetricType.CodeQuality && m.Abbreviation == "cc");
139+
Assert.Equal("Cyclomatic complexity", complexityMetric.Name);
140+
Assert.Equal(0, complexityMetric.Value);
141+
142+
var lineCoverageMetric = initMethodMetric.Metrics.Single(m => m.MetricType == MetricType.CoveragePercentual && m.Abbreviation == "cov");
143+
Assert.Equal("Line coverage", lineCoverageMetric.Name);
144+
Assert.Equal(100.0M, lineCoverageMetric.Value);
145+
146+
var branchCoverageMetric = initMethodMetric.Metrics.Single(m => m.MetricType == MetricType.CoveragePercentual && m.Abbreviation == "bcov");
147+
Assert.Equal("Branch coverage", branchCoverageMetric.Name);
148+
Assert.Equal(100.0M, branchCoverageMetric.Value);
149+
150+
var crapScoreMetric = initMethodMetric.Metrics.Single(m => m.MetricType == MetricType.CodeQuality && m.Abbreviation == "crp");
151+
Assert.Equal("Crap Score", crapScoreMetric.Name);
152+
Assert.Equal(0M, crapScoreMetric.Value);
153+
}
154+
155+
/// <summary>
156+
/// A test for MethodMetrics
157+
/// </summary>
158+
[Theory]
159+
[InlineData("Test", "Test.AbstractClass", "C:\\temp\\AbstractClass.cs", ".ctor()", 1, 1, 100, 100, 1)]
160+
[InlineData("Test", "Test.AbstractClass_SampleImpl1", "C:\\temp\\AbstractClass.cs", "Method1()", 3, 1, 0, 100, 2)]
161+
[InlineData("Test", "Test.PartialClass", "C:\\temp\\PartialClass.cs", "set_SomeProperty(System.Int32)", 4, 2, 66.66, 50, 2.15)]
162+
[InlineData("Test", "Test.Program", "C:\\temp\\Program.cs", "Main(System.String[])", 4, 1, 89.65, 100, 1.00)]
163+
[InlineData("Test", "Test.TestClass", "C:\\temp\\TestClass.cs", "SampleFunction()", 5, 4, 80, 50, 4.13)]
164+
public void MethodMetricsTest_2(string assemblyName, string className, string filePath, string methodName, int expectedMethodMetrics, double expectedComplexity, double expectedLineCoverage, double expectedBranchCoverage, double expectedCrapScore)
165+
{
166+
var methodMetrics = csharpParserResult
167+
.Assemblies.Single(a => a.Name == assemblyName)
168+
.Classes.Single(c => c.Name == className)
169+
.Files.Single(f => f.Path == filePath)
170+
.MethodMetrics.ToArray();
171+
172+
Assert.Equal(expectedMethodMetrics, methodMetrics.Length);
173+
174+
var methodMetric = methodMetrics.First(m => m.FullName == methodName);
175+
Assert.Equal(methodName, methodMetric.FullName);
176+
Assert.Equal(4, methodMetric.Metrics.Count());
177+
178+
var complexityMetric = methodMetric.Metrics.Single(m => m.MetricType == MetricType.CodeQuality && m.Abbreviation == "cc");
179+
Assert.Equal("Cyclomatic complexity", complexityMetric.Name);
180+
Assert.True(complexityMetric.Value.HasValue);
181+
Assert.Equal((decimal)expectedComplexity, complexityMetric.Value);
182+
183+
var lineCoverageMetric = methodMetric.Metrics.Single(m => m.MetricType == MetricType.CoveragePercentual && m.Abbreviation == "cov");
184+
Assert.Equal("Line coverage", lineCoverageMetric.Name);
185+
Assert.True(lineCoverageMetric.Value.HasValue);
186+
Assert.Equal((decimal)expectedLineCoverage, lineCoverageMetric.Value);
187+
188+
var branchCoverageMetric = methodMetric.Metrics.Single(m => m.MetricType == MetricType.CoveragePercentual && m.Abbreviation == "bcov");
189+
Assert.Equal("Branch coverage", branchCoverageMetric.Name);
190+
Assert.True(branchCoverageMetric.Value.HasValue);
191+
Assert.Equal((decimal)expectedBranchCoverage, branchCoverageMetric.Value);
192+
193+
var crapScoreMetric = methodMetric.Metrics.Single(m => m.MetricType == MetricType.CodeQuality && m.Abbreviation == "crp");
194+
Assert.Equal("Crap Score", crapScoreMetric.Name);
195+
Assert.True(crapScoreMetric.Value.HasValue);
196+
Assert.Equal((decimal)expectedCrapScore, crapScoreMetric.Value);
143197
}
144198

145199
/// <summary>
@@ -148,7 +202,7 @@ public void MethodMetricsTest()
148202
[Fact]
149203
public void CodeElementsTest()
150204
{
151-
var codeElements = GetFile(this.parserResult.Assemblies, "test.TestClass", "C:\\temp\\test\\TestClass.java").CodeElements;
205+
var codeElements = GetFile(this.javaParserResult.Assemblies, "test.TestClass", "C:\\temp\\test\\TestClass.java").CodeElements;
152206
Assert.Equal(4, codeElements.Count());
153207
}
154208

@@ -162,5 +216,15 @@ private static FileAnalysis GetFileAnalysis(IEnumerable<Assembly> assemblies, st
162216
.Single(c => c.Name == className).Files
163217
.Single(f => f.Path == fileName)
164218
.AnalyzeFile(new CachingFileReader(new LocalFileReader(), 0, null));
219+
220+
private static ParserResult ParseReport(string filePath)
221+
{
222+
var filterMock = new Mock<IFilter>();
223+
filterMock.Setup(f => f.IsElementIncludedInReport(It.IsAny<string>())).Returns(true);
224+
225+
var report = XDocument.Load(filePath);
226+
new CoberturaReportPreprocessor().Execute(report);
227+
return new CoberturaParser(filterMock.Object, filterMock.Object, filterMock.Object).Parse(report.Root);
228+
}
165229
}
166230
}

src/ReportGenerator.Core/Parser/CoberturaParser.cs

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -324,46 +324,46 @@ private static void SetMethodMetrics(CodeFile codeFile, IEnumerable<XElement> me
324324

325325
var lineRate = method.Attribute("line-rate");
326326

327+
decimal? coveragePercent = null;
327328
if (lineRate != null)
328329
{
329-
decimal? value = null;
330+
coveragePercent = GetCoberturaDecimalPercentageValue(lineRate.Value);
330331

331-
if (!"NaN".Equals(lineRate.Value, StringComparison.OrdinalIgnoreCase))
332-
{
333-
value = Math.Round(100 * decimal.Parse(lineRate.Value.Replace(',', '.'), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture), 2, MidpointRounding.AwayFromZero);
334-
}
335-
336-
metrics.Add(Metric.Coverage(value));
332+
metrics.Add(Metric.Coverage(coveragePercent));
337333
}
338334

339335
var branchRate = method.Attribute("branch-rate");
340336

341337
if (branchRate != null)
342338
{
343-
decimal? value = null;
344-
345-
if (!"NaN".Equals(branchRate.Value, StringComparison.OrdinalIgnoreCase))
346-
{
347-
value = Math.Round(100 * decimal.Parse(branchRate.Value.Replace(',', '.'), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture), 2, MidpointRounding.AwayFromZero);
348-
}
339+
decimal? value = GetCoberturaDecimalPercentageValue(branchRate.Value);
349340

350341
metrics.Add(Metric.BranchCoverage(value));
351342
}
352343

353344
var cyclomaticComplexityAttribute = method.Attribute("complexity");
354-
345+
decimal? cyclomaticComplexity = null;
355346
if (cyclomaticComplexityAttribute != null)
356347
{
357-
decimal? value = null;
358-
359-
if (!"NaN".Equals(cyclomaticComplexityAttribute.Value, StringComparison.OrdinalIgnoreCase))
360-
{
361-
value = Math.Round(decimal.Parse(cyclomaticComplexityAttribute.Value.Replace(',', '.'), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture), 2, MidpointRounding.AwayFromZero);
362-
}
348+
cyclomaticComplexity = GetCoberturaDecimalValue(cyclomaticComplexityAttribute.Value);
363349

364350
metrics.Insert(
365351
0,
366-
Metric.CyclomaticComplexity(value));
352+
Metric.CyclomaticComplexity(cyclomaticComplexity));
353+
}
354+
355+
if (cyclomaticComplexity.HasValue && coveragePercent.HasValue)
356+
{
357+
// https://testing.googleblog.com/2011/02/this-code-is-crap.html
358+
// CRAP(m) = CC(m)^2 * U(m)^3 + CC(m)
359+
// CC(m) <= Cyclomatic Complexity (e.g. 5)
360+
// U(m) <= Uncovered percentage (e.g. 30% = 0.3)
361+
var uncoveredPercent = (100f - (double)coveragePercent.Value) / 100.0;
362+
var complexity = (double)cyclomaticComplexity.Value;
363+
var crapScore = (Math.Pow(complexity, 2.0) * Math.Pow(uncoveredPercent, 3)) + complexity;
364+
crapScore = Math.Round(crapScore, 2, MidpointRounding.AwayFromZero);
365+
366+
metrics.Insert(0, Metric.CrapScore((decimal)crapScore));
367367
}
368368

369369
var methodMetric = new MethodMetric(fullName, shortName, metrics);
@@ -382,6 +382,39 @@ private static void SetMethodMetrics(CodeFile codeFile, IEnumerable<XElement> me
382382
}
383383
}
384384

385+
private static decimal? ParseCoberturaDecimalValue(string value)
386+
{
387+
decimal? result = null;
388+
if (!"NaN".Equals(value, StringComparison.OrdinalIgnoreCase))
389+
{
390+
result = decimal.Parse(value.Replace(',', '.'), NumberStyles.Number | NumberStyles.AllowExponent, CultureInfo.InvariantCulture);
391+
}
392+
393+
return result;
394+
}
395+
396+
private static decimal? GetCoberturaDecimalValue(string value)
397+
{
398+
decimal? result = ParseCoberturaDecimalValue(value);
399+
if (result.HasValue)
400+
{
401+
result = Math.Round(result.Value, 2, MidpointRounding.AwayFromZero);
402+
}
403+
404+
return result;
405+
}
406+
407+
private static decimal? GetCoberturaDecimalPercentageValue(string value)
408+
{
409+
decimal? result = ParseCoberturaDecimalValue(value);
410+
if (result.HasValue)
411+
{
412+
result = Math.Round(result.Value * 100, 2, MidpointRounding.AwayFromZero);
413+
}
414+
415+
return result;
416+
}
417+
385418
/// <summary>
386419
/// Extracts the methods/properties of the given <see cref="XElement">XElements</see>.
387420
/// </summary>

0 commit comments

Comments
 (0)