Skip to content

Commit

Permalink
TestEngine: Get instruction coverage and move to net standard (neo-pr…
Browse files Browse the repository at this point in the history
…oject#898)

* Get execution coverage

* Improve coverage comments

* Get coverage

* GetCoverage extension

* format

* Net standard and CoveredPercentage

* AbiMethod

* clean

* Allow coverage on Read&Write properties

* format

* Update README.md

* Allow to sum coverages

* clean

* Allow to sum coverage contracts

* Allow to join coverage from multiple sources

* change to public and ready to review

* Format and load methods in contract

* LF

* Rename to CoverageHit

* Enable or disable recover coverage

* Fix hit
  • Loading branch information
shargon authored Feb 15, 2024
1 parent ed7a67e commit 502b294
Show file tree
Hide file tree
Showing 17 changed files with 1,087 additions and 9 deletions.
94 changes: 94 additions & 0 deletions src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;

namespace Neo.SmartContract.Testing.Coverage
{
[DebuggerDisplay("{Name},{PCount}")]
public class AbiMethod : IEquatable<AbiMethod>
{
/// <summary>
/// Method name
/// </summary>
public string Name { get; }

/// <summary>
/// Parameters count
/// </summary>
public int PCount { get; }

/// <summary>
/// Constructor
/// </summary>
/// <param name="name">Method name</param>
/// <param name="pCount">Parameters count</param>
public AbiMethod(string name, int pCount)
{
Name = name;
PCount = pCount;
}

/// <summary>
/// Create from expression
/// </summary>
/// <param name="expression">Expression</param>
/// <returns>AbiMethod</returns>
public static AbiMethod[] CreateFromExpression(Expression expression)
{
if (expression is MemberExpression memberExpression)
{
if (memberExpression.Member is PropertyInfo pInfo)
{
if (pInfo.CanRead)
{
var display = pInfo.GetGetMethod()?.GetCustomAttribute<DisplayNameAttribute>();
var nameRead = display is not null ? display.DisplayName : memberExpression.Member.Name;

if (pInfo.CanWrite)
{
// If Property CanWrite, we return both methods

display = pInfo.GetSetMethod()?.GetCustomAttribute<DisplayNameAttribute>();
var nameWrite = display is not null ? display.DisplayName : memberExpression.Member.Name;

return new AbiMethod[]
{
new AbiMethod(nameRead, 0),
new AbiMethod(nameWrite, 1)
};
}

// Only read property

return new AbiMethod[] { new AbiMethod(nameRead, 0) };
}
}
}
else if (expression is MethodCallExpression methodExpression)
{
if (methodExpression.Method is MethodInfo mInfo)
{
var display = mInfo.GetCustomAttribute<DisplayNameAttribute>();
var name = display is not null ? display.DisplayName : mInfo.Name;

return new AbiMethod[] { new AbiMethod(name, mInfo.GetParameters().Length) };
}
}

return Array.Empty<AbiMethod>();
}

public override bool Equals(object obj)
{
if (obj is not AbiMethod other) return false;

return PCount == other.PCount && Name == other.Name;
}

bool IEquatable<AbiMethod>.Equals(AbiMethod other) => PCount == other.PCount && Name == other.Name;
public override int GetHashCode() => HashCode.Combine(PCount, Name);
public override string ToString() => $"{Name},{PCount}";
}
}
79 changes: 79 additions & 0 deletions src/Neo.SmartContract.Testing/Coverage/CoverageBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Neo.SmartContract.Testing.Coverage
{
public abstract class CoverageBase : IEnumerable<CoverageHit>
{
/// <summary>
/// Coverage
/// </summary>
public abstract IEnumerable<CoverageHit> Coverage { get; }

/// <summary>
/// Total instructions (could be different from Coverage.Count if, for example, a contract JUMPS to PUSHDATA content)
/// </summary>
public virtual int TotalInstructions => Coverage.Where(u => !u.OutOfScript).Count();

/// <summary>
/// Covered Instructions (OutOfScript are not taken into account)
/// </summary>
public virtual int CoveredInstructions => Coverage.Where(u => !u.OutOfScript && u.Hits > 0).Count();

/// <summary>
/// All instructions that have been touched
/// </summary>
public virtual int HitsInstructions => Coverage.Where(u => u.Hits > 0).Count();

/// <summary>
/// Covered Percentage
/// </summary>
public float CoveredPercentage
{
get
{
var total = TotalInstructions;
if (total == 0) return 0F;

return (float)CoveredInstructions / total * 100F;
}
}

/// <summary>
/// Get Coverage from the Contract coverage
/// </summary>
/// <param name="offset">Offset</param>
/// <param name="length">Length</param>
/// <returns>Coverage</returns>
public IEnumerable<CoverageHit> GetCoverageFrom(int offset, int length)
{
var to = offset + length;

foreach (var kvp in Coverage)
{
if (kvp.Offset >= offset && kvp.Offset <= to)
{
yield return kvp;
}
}
}

#region IEnumerable

public IEnumerator<CoverageHit> GetEnumerator() => Coverage.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Coverage.GetEnumerator();

#endregion

// Allow to sum coverages

public static CoverageBase? operator +(CoverageBase? a, CoverageBase? b)
{
if (a is null) return b;
if (b is null) return a;

return new CoveredCollection(a, b);
}
}
}
125 changes: 125 additions & 0 deletions src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Diagnostics;

namespace Neo.SmartContract.Testing.Coverage
{
[DebuggerDisplay("Offset:{Offset}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}")]
public class CoverageHit
{
/// <summary>
/// The instruction offset
/// </summary>
public int Offset { get; }

/// <summary>
/// The instruction is out of the script
/// </summary>
public bool OutOfScript { get; }

/// <summary>
/// Hits
/// </summary>
public int Hits { get; private set; }

/// <summary>
/// Minimum used gas
/// </summary>
public long GasMin { get; private set; }

/// <summary>
/// Minimum used gas
/// </summary>
public long GasMax { get; private set; }

/// <summary>
/// Total used gas
/// </summary>
public long GasTotal { get; private set; }

/// <summary>
/// Average used gas
/// </summary>
public long GasAvg => Hits == 0 ? 0 : GasTotal / Hits;

/// <summary>
/// Constructor
/// </summary>
/// <param name="offset">Offset</param>
/// <param name="outOfScript">Out of script</param>
public CoverageHit(int offset, bool outOfScript = false)
{
Offset = offset;
OutOfScript = outOfScript;
}

/// <summary>
/// Hits
/// </summary>
/// <param name="gas">Gas</param>
public void Hit(long gas)
{
Hits++;

if (Hits == 1)
{
GasMin = gas;
GasMax = gas;
}
else
{
GasMin = Math.Min(GasMin, gas);
GasMax = Math.Max(GasMax, gas);
}

GasTotal += gas;
}

/// <summary>
/// Hits
/// </summary>
/// <param name="value">Value</param>
public void Hit(CoverageHit value)
{
if (value.Hits == 0) return;

Hits += value.Hits;

if (Hits == 1)
{
GasMin = value.GasMin;
GasMax = value.GasMax;
}
else
{
GasMin = Math.Min(GasMin, value.GasMin);
GasMax = Math.Max(GasMax, value.GasMax);
}

GasTotal += value.GasTotal;
}

/// <summary>
/// Clone data
/// </summary>
/// <returns>CoverageData</returns>
public CoverageHit Clone()
{
return new CoverageHit(Offset, OutOfScript)
{
GasMax = GasMax,
GasMin = GasMin,
GasTotal = GasTotal,
Hits = Hits
};
}

/// <summary>
/// String representation
/// </summary>
/// <returns></returns>
public override string ToString()
{
return $"Offset:{Offset}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}";
}
}
}
38 changes: 38 additions & 0 deletions src/Neo.SmartContract.Testing/Coverage/CoveredCollection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Collections.Generic;

namespace Neo.SmartContract.Testing.Coverage
{
public class CoveredCollection : CoverageBase
{
/// <summary>
/// Entries
/// </summary>
public CoverageBase[] Entries { get; }

/// <summary>
/// Coverage
/// </summary>
public override IEnumerable<CoverageHit> Coverage
{
get
{
foreach (var method in Entries)
{
foreach (var entry in method.Coverage)
{
yield return entry;
}
}
}
}

/// <summary>
/// Constructor
/// </summary>
/// <param name="entries">Entries</param>
public CoveredCollection(params CoverageBase[] entries)
{
Entries = entries;
}
}
}
Loading

0 comments on commit 502b294

Please sign in to comment.