Skip to content

Commit

Permalink
Add cobertura report
Browse files Browse the repository at this point in the history
  • Loading branch information
lucaslorentz committed Nov 23, 2019
1 parent bd3cd31 commit cf96382
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 8 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,4 @@ coverage-hits
opencovercoverage.xml
clover.xml
coveralls.json
cobertura.xml
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ The command exits with failure if the coverage doesn't meet a specific threshold
### More commands

- **cloverreport**: Write an Clover-formatted XML report to file
- **coberturareport**: Write a cobertura XML report to file
- **coverallsreport**: Prepare and/or submit coveralls reports
- **htmlreport**: Write html report to folder
- **opencoverreport**: Write an OpenCover-formatted XML report to file
Expand Down
5 changes: 5 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ jobs:
- bash: |
./build-sample.sh
displayName: 'Build Sample'
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(Build.SourcesDirectory)/cobertura.xml'

- job: Build_Linux
pool:
Expand Down
3 changes: 3 additions & 0 deletions build-sample.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,7 @@ echo "# End OpenCoverReport"
echo "# Start CloverReport"
dotnet minicover cloverreport --threshold 60
echo "# End CloverReport"
echo "# Start CoberturaReport"
dotnet minicover coberturareport --threshold 60
echo "# End CoberturaReport"
cd ..
8 changes: 7 additions & 1 deletion build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,21 @@ rm -r coverage || true
dotnet build

echo "# Start Instrument"
./minicover.sh instrument --tests ""
./minicover.sh instrument --assemblies "**/netcoreapp3.0/**/*.dll" --tests ""
echo "# End Instrument"

./minicover.sh reset

dotnet test --no-build

echo "# Start Uninstrument"
./minicover.sh uninstrument
echo "# End Uninstrument"

echo "# Start CoberturaReport"
./minicover.sh coberturareport --threshold 0
echo "# End CoberturaReport"

echo "# Start Report"
./minicover.sh report --threshold 0
echo "# End Report"
Expand Down
44 changes: 44 additions & 0 deletions src/MiniCover/CommandLine/Commands/CoberturaReportCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Threading.Tasks;
using MiniCover.CommandLine.Options;
using MiniCover.Reports;
using MiniCover.Utils;

namespace MiniCover.CommandLine.Commands
{
class CoberturaReportCommand : BaseCommand
{
private const string _name = "coberturareport";
private const string _description = "Write a Cobertura-formatted XML report to file";

private readonly CoverageLoadedFileOption _coverageLoadedFileOption;
private readonly CoberturaOutputOption _coberturaOutputOption;
private readonly ThresholdOption _thresholdOption;

public CoberturaReportCommand(
WorkingDirectoryOption workingDirectoryOption,
CoverageLoadedFileOption coverageLoadedFileOption,
CoberturaOutputOption coberturaOutputOption,
ThresholdOption thresholdOption)
: base(_name, _description)
{
_coverageLoadedFileOption = coverageLoadedFileOption;
_thresholdOption = thresholdOption;
_coberturaOutputOption = coberturaOutputOption;

Options = new IOption[]
{
workingDirectoryOption,
_coverageLoadedFileOption,
_thresholdOption,
_coberturaOutputOption
};
}

protected override Task<int> Execute()
{
new CoberturaReport().Execute(_coverageLoadedFileOption.Result, _coberturaOutputOption.Value);
var result = CalcUtils.IsHigherThanThreshold(_coverageLoadedFileOption.Result, _thresholdOption.Value);
return Task.FromResult(result);
}
}
}
2 changes: 1 addition & 1 deletion src/MiniCover/CommandLine/Commands/NCoverReportCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public NCoverReportCommand(

protected override Task<int> Execute()
{
XmlReport.Execute(_coverageLoadedFileOption.Result, _nCoverOutputOption.Value, _thresholdOption.Value);
NCoverReport.Execute(_coverageLoadedFileOption.Result, _nCoverOutputOption.Value, _thresholdOption.Value);
var result = CalcUtils.IsHigherThanThreshold(_coverageLoadedFileOption.Result, _thresholdOption.Value);
return Task.FromResult(result);
}
Expand Down
19 changes: 19 additions & 0 deletions src/MiniCover/CommandLine/Options/CoberturaOutputOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace MiniCover.CommandLine.Options
{
class CoberturaOutputOption : FileOption
{
private const string _defaultValue = "./cobertura.xml";
private const string _template = "--output";
private static readonly string _description = $"Output file for cobertura report [default: {_defaultValue}]";

public CoberturaOutputOption()
: base(_template, _description)
{
}

protected override string GetDefaultValue()
{
return _defaultValue;
}
}
}
15 changes: 15 additions & 0 deletions src/MiniCover/Extensions/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace MiniCover.Extensions
{
public static class EnumerableExtensions
{
public static IEnumerable<IGrouping<TKey, T>> GroupByMany<T, TKey>(this IEnumerable<T> source, Func<T, IEnumerable<TKey>> keysSelector)
{
return source.SelectMany(x => keysSelector(x), (x, k) => new { x, k })
.GroupBy(j => j.k, j => j.x);
}
}
}
8 changes: 7 additions & 1 deletion src/MiniCover/Model/SourceFile.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MiniCover.Model
Expand All @@ -14,5 +15,10 @@ public static SourceFile Merge(IEnumerable<SourceFile> sources)
}

public List<InstrumentedInstruction> Instructions = new List<InstrumentedInstruction>();

internal object SelectMany(Func<object, object> p)
{
throw new NotImplementedException();
}
}
}
2 changes: 2 additions & 0 deletions src/MiniCover/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private static IServiceProvider ConfigureServices(IOutput output)
services.AddTransient<BaseCommand, OpenCoverReportCommand>();
services.AddTransient<BaseCommand, CloverReportCommand>();
services.AddTransient<BaseCommand, CoverallsReportCommand>();
services.AddTransient<BaseCommand, CoberturaReportCommand>();

services.AddTransient<WorkingDirectoryOption>();
services.AddTransient<ParentDirectoryOption>();
Expand All @@ -106,6 +107,7 @@ private static IServiceProvider ConfigureServices(IOutput output)
services.AddTransient<HtmlOutputFolderOption>();
services.AddTransient<NCoverOutputOption>();
services.AddTransient<OpenCoverOutputOption>();
services.AddTransient<CoberturaOutputOption>();
services.AddTransient<VerbosityOption>();

services.AddSingleton<Instrumenter>();
Expand Down
197 changes: 197 additions & 0 deletions src/MiniCover/Reports/CoberturaReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using MiniCover.Extensions;
using MiniCover.Model;

namespace MiniCover.Reports
{
public class CoberturaReport
{
public void Execute(InstrumentationResult result, FileInfo output)
{
var hits = HitsInfo.TryReadFromDirectory(result.HitsPath);

var document = new XDocument(
new XDeclaration("1.0", "utf-8", null),
new XDocumentType("coverage", null, "http://cobertura.sourceforge.net/xml/coverage-04.dtd", null),
CreateCoverageElement(result, hits)
);

var xmlWriterSettings = new XmlWriterSettings
{
Indent = true
};

output.Directory.Create();

using (var sw = output.CreateText())
using (var writer = XmlWriter.Create(sw, xmlWriterSettings))
{
document.WriteTo(writer);
}
}

private static XElement CreateCoverageElement(InstrumentationResult result, HitsInfo hitsInfo)
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();

var linesValid = result.GetSourceFiles()
.SelectMany(kvFile => kvFile.Value.Instructions)
.SelectMany(i => i.GetLines())
.Distinct()
.Count();

var coveredLines = result.GetSourceFiles()
.SelectMany(kvFile => kvFile.Value.Instructions)
.Where(h => hitsInfo.IsInstructionHit(h.Id))
.SelectMany(i => i.GetLines())
.Distinct()
.Count();

var linesRate = linesValid == 0 ? 0d : (double)coveredLines / (double)linesValid;

return new XElement(
XName.Get("coverage"),
new XAttribute(XName.Get("lines-valid"), linesValid),
new XAttribute(XName.Get("lines-covered"), coveredLines),
new XAttribute(XName.Get("line-rate"), linesRate),
new XAttribute(XName.Get("timestamp"), timestamp),
new XAttribute(XName.Get("version"), "1.0"),
CrateSourcesElement(result, hitsInfo),
CratePackagesElement(result, hitsInfo)
);
}

private static XElement CrateSourcesElement(InstrumentationResult result, HitsInfo hitsInfo)
{
return new XElement(
XName.Get("sources"),
new XElement("source",
new XText(result.SourcePath)
)
);
}

private static XElement CratePackagesElement(InstrumentationResult result, HitsInfo hitsInfo)
{
return new XElement(
XName.Get("packages"),
result.Assemblies
.Where(a => a.SourceFiles.Count > 0)
.Select(a => CreatePackageElement(a, hitsInfo))
);
}

private static XElement CreatePackageElement(InstrumentedAssembly assembly, HitsInfo hitsInfo)
{
var linesValid = assembly.SourceFiles
.SelectMany(kvFile => kvFile.Value.Instructions)
.SelectMany(i => i.GetLines())
.Distinct()
.Count();

var coveredLines = assembly.SourceFiles
.SelectMany(kvFile => kvFile.Value.Instructions)
.Where(h => hitsInfo.IsInstructionHit(h.Id))
.SelectMany(i => i.GetLines())
.Distinct()
.Count();

var linesRate = linesValid == 0 ? 0d : (double)coveredLines / (double)linesValid;

return new XElement(
XName.Get("package"),
new XAttribute(XName.Get("name"), assembly.Name),
new XAttribute(XName.Get("line-rate"), linesRate),
CreateClassesElement(assembly, hitsInfo)
);
}

private static XElement CreateClassesElement(InstrumentedAssembly assembly, HitsInfo hitsInfo)
{
return new XElement(
XName.Get("classes"),
assembly.SourceFiles
.Select(kv => CreateClassElement(kv.Key, kv.Value, hitsInfo))
);
}

private static XElement CreateClassElement(string fileName, SourceFile sourceFile, HitsInfo hitsInfo)
{
var linesValid = sourceFile.Instructions
.SelectMany(i => i.GetLines())
.Distinct()
.Count();

var coveredLines = sourceFile.Instructions
.Where(h => hitsInfo.IsInstructionHit(h.Id))
.SelectMany(i => i.GetLines())
.Distinct()
.Count();

var linesRate = (double)coveredLines / (double)linesValid;

return new XElement(

XName.Get("class"),
new XAttribute(XName.Get("name"), fileName),
new XAttribute(XName.Get("filename"), fileName),
new XAttribute(XName.Get("line-rate"), linesRate),
CreateMethodsElement(sourceFile, hitsInfo),
CreateLinesElement(sourceFile.Instructions, hitsInfo)
);
}

private static XElement CreateMethodsElement(SourceFile sourceFile, HitsInfo hitsInfo)
{
return new XElement(
XName.Get("methods"),
sourceFile.Instructions
.GroupBy(i => i.Method)
.Select(g => CreateMethodElement(g.Key, g, hitsInfo))
);
}

private static XElement CreateMethodElement(InstrumentedMethod method, IEnumerable<InstrumentedInstruction> instructions, HitsInfo hitsInfo)
{
var hits = instructions.Sum(i => hitsInfo.GetInstructionHitCount(i.Id));

var openParametersIndex = method.FullName.IndexOf("(");
var signature = method.FullName.Substring(openParametersIndex);

return new XElement(
XName.Get("method"),
new XAttribute(XName.Get("name"), method.Name),
new XAttribute(XName.Get("signature"), signature),
new XAttribute(XName.Get("hits"), hits),
CreateLinesElement(instructions, hitsInfo)
);
}

private static XElement CreateLinesElement(IEnumerable<InstrumentedInstruction> instructions, HitsInfo hitsInfo)
{
return new XElement(
XName.Get("lines"),
instructions
.GroupByMany(i => i.GetLines())
.OrderBy(g => g.Key)
.Select(g => CreateLineElement(g.Key, g, hitsInfo))
);
}

private static object CreateLineElement(int line, IEnumerable<InstrumentedInstruction> instructions, HitsInfo hitsInfo)
{
var hits = instructions.Sum(i => hitsInfo.GetInstructionHitCount(i.Id));

return new XElement(
XName.Get("line"),
new XAttribute(XName.Get("number"), line),
new XAttribute(XName.Get("hits"), hits)
);
}
}
}
8 changes: 4 additions & 4 deletions src/MiniCover/Reports/Coveralls/CoverallsReport.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using MiniCover.Model;
using MiniCover.Extensions;
using MiniCover.Model;
using MiniCover.Reports.Coveralls.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand Down Expand Up @@ -118,9 +119,8 @@ public virtual async Task<int> Execute(InstrumentationResult result)
var sourceLines = File.ReadAllLines(sourceFile);

var hitsPerLine = kvFile.Value.Instructions
.SelectMany(i => i.GetLines(), (instruction, line) => new { instruction, line })
.GroupBy(i => i.line)
.ToDictionary(g => g.Key, g => g.Sum(j => hits.GetInstructionHitCount(j.instruction.Id)));
.GroupByMany(i => i.GetLines())
.ToDictionary(g => g.Key, g => g.Sum(i => hits.GetInstructionHitCount(i.Id)));

var fileName = Path.GetRelativePath(_rootFolder, sourceFile).Replace("\\", "/");

Expand Down
Loading

0 comments on commit cf96382

Please sign in to comment.