Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TestEngine: Source code coverage #937

Merged
merged 6 commits into from
Feb 22, 2024
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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
dotnet new uninstall Neo.SmartContract.Template
dotnet remove ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj package Neo.SmartContract.Framework
dotnet add ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj reference ./src/Neo.SmartContract.Framework/Neo.SmartContract.Framework.csproj
dotnet ./src/Neo.Compiler.CSharp/bin/Debug/net7.0/nccs.dll -d ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj -o ./tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Artifacts/ --generate-artifacts source
dotnet ./src/Neo.Compiler.CSharp/bin/Debug/net7.0/nccs.dll ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj -o ./tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Artifacts/ --generate-artifacts source --debug
shargon marked this conversation as resolved.
Show resolved Hide resolved
- name: Build Solution
run: dotnet build ./neo-devpack-dotnet.sln
- name: Check format
Expand Down
25 changes: 5 additions & 20 deletions src/Neo.SmartContract.Testing/Coverage/CoverageBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,30 +48,12 @@ public abstract class CoverageBase
/// <summary>
/// Covered lines percentage
/// </summary>
public float CoveredLinesPercentage
{
get
{
var total = TotalLines;
if (total == 0) return 1F;

return (float)CoveredLines / total;
}
}
public decimal CoveredLinesPercentage => CalculateHitRate(TotalLines, CoveredLines);

/// <summary>
/// Covered branch percentage
/// </summary>
public float CoveredBranchPercentage
{
get
{
var total = TotalBranches;
if (total == 0) return 1F;

return (float)CoveredBranches / total;
}
}
public decimal CoveredBranchPercentage => CalculateHitRate(TotalBranches, CoveredBranches);

/// <summary>
/// Get Coverage lines from the Contract coverage
Expand Down Expand Up @@ -111,6 +93,9 @@ public IEnumerable<CoverageBranch> GetCoverageBranchFrom(int offset, int length)
}
}

public static decimal CalculateHitRate(int total, int hits)
=> total == 0 ? 1m : new decimal(hits) / new decimal(total);
vncoelho marked this conversation as resolved.
Show resolved Hide resolved

// Allow to sum coverages

public static CoverageBase? operator +(CoverageBase? a, CoverageBase? b)
Expand Down
28 changes: 28 additions & 0 deletions src/Neo.SmartContract.Testing/Coverage/CoverageReporting.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Palmmedia.ReportGenerator.Core;
using System.IO;

namespace Neo.SmartContract.Testing.Coverage
{
public class CoverageReporting
{
/// <summary>
/// Generate report from cobertura
/// </summary>
/// <param name="file">Coverage file</param>
/// <param name="outputDir">Output dir</param>
/// <returns>True if was success</returns>
public static bool CreateReport(string file, string outputDir)
{
try
{
// Reporting

Program.Main(new string[] { $"-reports:{Path.GetFullPath(file)}", $"-targetdir:{Path.GetFullPath(outputDir)}" });
return true;
}
catch { }

return false;
}
}
}
175 changes: 40 additions & 135 deletions src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Neo.SmartContract.Manifest;
using Neo.SmartContract.Testing.Coverage.Formats;
using Neo.VM;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text;
Expand All @@ -14,8 +16,8 @@ public class CoveredContract : CoverageBase
{
#region Internal

private readonly Dictionary<int, CoverageHit> _lines = new();
private readonly Dictionary<int, CoverageBranch> _branches = new();
private readonly SortedDictionary<int, CoverageHit> _lines = new();
private readonly SortedDictionary<int, CoverageBranch> _branches = new();

#endregion

Expand Down Expand Up @@ -215,6 +217,16 @@ private CoveredMethod CreateMethod(
return Methods.FirstOrDefault(m => m.Method.Equals(method));
}

internal bool TryGetLine(int offset, [NotNullWhen(true)] out CoverageHit? lineHit)
{
return _lines.TryGetValue(offset, out lineHit);
}

internal bool TryGetBranch(int offset, [NotNullWhen(true)] out CoverageBranch? branch)
{
return _branches.TryGetValue(offset, out branch);
}

/// <summary>
/// Join coverage
/// </summary>
Expand Down Expand Up @@ -277,150 +289,43 @@ public string Dump(DumpFormat format = DumpFormat.Console)
/// <returns>Coverage dump</returns>
internal string Dump(DumpFormat format, params CoveredMethod[] methods)
{
var builder = new StringBuilder();
using var sourceCode = new StringWriter(builder)
{
NewLine = "\n"
};

switch (format)
{
case DumpFormat.Console:
{
var coverLines = $"{CoveredLinesPercentage:P2}";
var coverBranch = $"{CoveredBranchPercentage:P2}";
sourceCode.WriteLine($"{Hash} [{coverLines} - {coverBranch}]");

List<string[]> rows = new();
var max = new int[] { "Method".Length, "Line ".Length, "Branch".Length };

foreach (var method in methods.OrderBy(u => u.Method.Name).OrderByDescending(u => u.CoveredLinesPercentage))
{
coverLines = $"{method.CoveredLinesPercentage:P2}";
coverBranch = $"{method.CoveredBranchPercentage:P2}";
rows.Add(new string[] { method.Method.ToString(), coverLines, coverBranch });

max[0] = Math.Max(method.Method.ToString().Length, max[0]);
max[1] = Math.Max(coverLines.Length, max[1]);
max[2] = Math.Max(coverLines.Length, max[2]);
}

sourceCode.WriteLine($"┌-{"─".PadLeft(max[0], '─')}-┬-{"─".PadLeft(max[1], '─')}-┬-{"─".PadLeft(max[1], '─')}-┐");
sourceCode.WriteLine($"│ {string.Format($"{{0,-{max[0]}}}", "Method", max[0])} │ {string.Format($"{{0,{max[1]}}}", "Line ", max[1])} │ {string.Format($"{{0,{max[2]}}}", "Branch", max[1])} │");
sourceCode.WriteLine($"├-{"─".PadLeft(max[0], '─')}-┼-{"─".PadLeft(max[1], '─')}-┼-{"─".PadLeft(max[1], '─')}-┤");

foreach (var print in rows)
{
sourceCode.WriteLine($"│ {string.Format($"{{0,-{max[0]}}}", print[0], max[0])} │ {string.Format($"{{0,{max[1]}}}", print[1], max[1])} │ {string.Format($"{{0,{max[1]}}}", print[2], max[2])} │");
}

sourceCode.WriteLine($"└-{"─".PadLeft(max[0], '─')}-┴-{"─".PadLeft(max[1], '─')}-┴-{"─".PadLeft(max[2], '─')}-┘");
break;
return Dump(new ConsoleFormat(this, methods));
}
case DumpFormat.Html:
{
sourceCode.WriteLine(@"
<!DOCTYPE html>
<html lang=""en"">
<head>
<meta charset=""UTF-8"">
<title>NEF coverage Report</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; }
.bar { background-color: #f2f2f2; padding: 10px; cursor: pointer; }
.hash { float: left; }
.method-name { float: left; }
.coverage { float: right; display: inline-block; width: 100px; text-align: right; }
.method { cursor: pointer; margin-top: 5px; padding: 2px; }
.details { display: none; padding-left: 20px; }
.container { padding-left: 20px; }
.opcode { margin-left: 20px; position: relative; padding: 2px; margin-bottom: 2px; display: flex; align-items: center; }
.hit { background-color: #eafaea; } /* Light green for hits */
.no-hit { background-color: #ffcccc; } /* Light red for no hits */
.hits { margin-left: 5px; font-size: 0.6em; margin-right: 10px; }
.branch { margin-left: 5px; font-size: 0.6em; margin-right: }
.icon { margin-right: 5px; }

.high-coverage { background-color: #ccffcc; } /* Lighter green for high coverage */
.medium-coverage { background-color: #ffffcc; } /* Yellow for medium coverage */
.low-coverage { background-color: #ffcccc; } /* Lighter red for low coverage */
</style>
</head>
<body>
");

sourceCode.WriteLine($@"
<div class=""bar"">
<div class=""hash"">{Hash}</div>
<div class=""coverage"">&nbsp;{CoveredBranchPercentage:P2}&nbsp;</div>
<div class=""coverage"">&nbsp;{CoveredLinesPercentage:P2}&nbsp;</div>
<div style=""clear: both;""></div>
</div>
<div class=""container"">
");

foreach (var method in methods.OrderBy(u => u.Method.Name).OrderByDescending(u => u.CoveredLinesPercentage))
{
var kind = "low";
if (method.CoveredLinesPercentage > 0.7) kind = "medium";
if (method.CoveredLinesPercentage > 0.8) kind = "high";

sourceCode.WriteLine($@"
<div class=""method {kind}-coverage"">
<div class=""method-name"">{method.Method}</div>
<div class=""coverage"">&nbsp;{method.CoveredBranchPercentage:P2}&nbsp;</div>
<div class=""coverage"">&nbsp;{method.CoveredLinesPercentage:P2}&nbsp;</div>
<div style=""clear: both;""></div>
</div>
");
sourceCode.WriteLine($@"<div class=""details"">");

foreach (var hit in method.Lines)
{
var noHit = hit.Hits == 0 ? "no-" : "";
var icon = hit.Hits == 0 ? "✘" : "✔";
var branch = "";

if (_branches.TryGetValue(hit.Offset, out var b))
{
branch = $" <span class=\"branch\">[ᛦ {b.Hits}/{b.Count}]</span>";
}

sourceCode.WriteLine($@"<div class=""opcode {noHit}hit""><span class=""icon"">{icon}</span><span class=""hits"">{hit.Hits} Hits</span>{hit.Description}{branch}</div>");
}

sourceCode.WriteLine($@"</div>
");
}

sourceCode.WriteLine(@"
</div>
<script>
document.querySelector('.bar').addEventListener('click', () => {
const container = document.querySelector('.container');
container.style.display = container.style.display === 'none' ? 'block' : 'none';
});

document.querySelectorAll('.method').forEach(item => {
item.addEventListener('click', function() {
const details = this.nextElementSibling;
if(details.style.display === '' || details.style.display === 'none') {
details.style.display = 'block';
} else {
details.style.display = 'none';
return Dump(new IntructionHtmlFormat(this, methods));
}
default:
{
throw new NotImplementedException();
}
}
});
});
</script>
}

</body>
</html>
");
break;
}
/// <summary>
/// Dump to format
/// </summary>
/// <param name="format">Format</param>
/// <param name="debugInfo">Debug Info</param>
/// <returns>Covertura</returns>
public string Dump(ICoverageFormat format)
{
Dictionary<string, string> outputMap = new();

void writeAttachment(string filename, Action<Stream> writestream)
{
using MemoryStream stream = new();
writestream(stream);
var text = Encoding.UTF8.GetString(stream.ToArray());
outputMap.Add(filename, text);
}

return builder.ToString();
format.WriteReport(writeAttachment);
return outputMap.First().Value;
}

/// <summary>
Expand Down
Loading
Loading