Skip to content

Commit

Permalink
Fix: compilation and coverage issues (#921)
Browse files Browse the repository at this point in the history
* Fix compiler length

* Fix method detection

* Fix ut transfer

* allow to check multiple

* Fix IList<object>

* Allow CALLT and different methods detection

* Clean code

* add comment

* clean

* Fix clean

* Allow to release the mock from engine

* Release mock

* Show error when access to storage without deploy

* remove 0x

* use params

* Dictionary

* Update NuGet.Config

* Update NuGet.Config

Co-authored-by: Jimmy <[email protected]>

* Update src/Neo.SmartContract.Testing/SmartContractStorage.cs

---------

Co-authored-by: Jimmy <[email protected]>
  • Loading branch information
shargon and Jim8y authored Feb 21, 2024
1 parent 2e6378a commit 83600e2
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 29 deletions.
4 changes: 2 additions & 2 deletions NuGet.Config
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<configuration>
<packageSources>
<clear />
<add key="github" value="https://nuget.pkg.github.com/neo-project/index.json" />
<add key="MyGet-neo" value="https://www.myget.org/F/neo/api/v3/index.json" />
<add key="NuGet.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
</configuration>
7 changes: 7 additions & 0 deletions src/Neo.Compiler.CSharp/CompilationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,13 @@ public NefFile CreateExecutable()
Tokens = methodTokens.ToArray(),
Script = Script
};

if (nef.Compiler.Length > 64)
{
// Neo.Compiler.CSharp 3.6.2+470d9a8608b41de658849994a258200d8abf7caa
nef.Compiler = nef.Compiler.Substring(0, 61) + "...";
}

nef.CheckSum = NefFile.ComputeChecksum(nef);
// Ensure that is serializable
return nef.ToArray().AsSerializable<NefFile>();
Expand Down
14 changes: 13 additions & 1 deletion src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,26 @@ public CoverageHit Clone()
/// </summary>
/// <param name="instruction">Instruction</param>
/// <returns>Description</returns>
public static string DescriptionFromInstruction(Instruction instruction)
public static string DescriptionFromInstruction(Instruction instruction, params MethodToken[]? tokens)
{
if (instruction.Operand.Length > 0)
{
var ret = instruction.OpCode.ToString() + " 0x" + instruction.Operand.ToArray().ToHexString();

switch (instruction.OpCode)
{
case OpCode.CALLT:
{
var tokenId = instruction.TokenU16;

if (tokens != null && tokens.Length > tokenId)
{
var token = tokens[tokenId];

return ret + $" ({token.Hash},{token.Method},{token.ParametersCount},{token.CallFlags})";
}
break;
}
case OpCode.JMP:
case OpCode.JMPIF:
case OpCode.JMPIFNOT:
Expand Down
111 changes: 90 additions & 21 deletions src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,26 @@ public class CoveredContract : CoverageBase
/// <summary>
/// CoveredContract
/// </summary>
/// <param name="mechanism">Method detection mechanism</param>
/// <param name="hash">Hash</param>
/// <param name="abi">Contract abi</param>
/// <param name="script">Script</param>
public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script)
/// <param name="state">Contract state</param>
public CoveredContract(MethodDetectionMechanism mechanism, UInt160 hash, ContractState? state)
{
Hash = hash;
Methods = Array.Empty<CoveredMethod>();

if (script is null) return;

// Extract all methods
if (state is not null)
{
// Extract all methods
GenerateMethods(mechanism, state);
}
}

GenerateMethods(abi, script);
internal void GenerateMethods(MethodDetectionMechanism mechanism, ContractState state)
{
Script script = state.Script;
HashSet<int> privateAdded = new();
List<ContractMethodDescriptor> methods = new(state.Manifest.Abi.Methods);

// Iterate all valid instructions

Expand All @@ -57,29 +64,91 @@ public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script)
while (ip < script.Length)
{
var instruction = script.GetInstruction(ip);
_coverageData[ip] = new CoverageHit(ip, CoverageHit.DescriptionFromInstruction(instruction), false);
ip += instruction.Size;
}
}

internal void GenerateMethods(ContractAbi? abi, Script? script)
{
Methods = Array.Empty<CoveredMethod>();
if (!_coverageData.ContainsKey(ip))
{
_coverageData[ip] = new CoverageHit(ip, CoverageHit.DescriptionFromInstruction(instruction, state.Nef.Tokens), false);
}

if (mechanism == MethodDetectionMechanism.NextMethod)
{
// Find private methods

switch (instruction.OpCode)
{
case OpCode.CALL_L:
{
var offset = ip + instruction.TokenI32;
if (privateAdded.Add(offset))
{
methods.Add(new ContractMethodDescriptor()
{
Name = "_private" + offset,
Offset = offset,
ReturnType = ContractParameterType.Void,
Safe = false,
Parameters = Array.Empty<ContractParameterDefinition>(),
});
}
break;
}
case OpCode.CALLT:
{
var offset = ip + instruction.TokenI8;
if (privateAdded.Add(offset))
{
methods.Add(new ContractMethodDescriptor()
{
Name = "_private" + offset,
Offset = offset,
ReturnType = ContractParameterType.Void,
Safe = false,
Parameters = Array.Empty<ContractParameterDefinition>(),
});
}
break;
}
}
}

if (script is null || abi is null) return;
ip += instruction.Size;
}

Methods = abi.Methods
.Select(s => CreateMethod(abi, script, s))
Methods = methods
.Select(s => CreateMethod(mechanism, script, methods, s))
.OrderBy(o => o.Offset)
.ToArray()!;
}

private CoveredMethod CreateMethod(ContractAbi abi, Script script, ContractMethodDescriptor abiMethod)
private CoveredMethod CreateMethod(
MethodDetectionMechanism mechanism, Script script,
List<ContractMethodDescriptor> allMethods, ContractMethodDescriptor abiMethod
)
{
int ip = abiMethod.Offset;
var to = script.Length - 1;
var next = abi.Methods.OrderBy(u => u.Offset).Where(u => u.Offset > abiMethod.Offset).FirstOrDefault();

if (next is not null) to = next.Offset - 1;
switch (mechanism)
{
case MethodDetectionMechanism.FindRET:
{
while (ip < script.Length)
{
var instruction = script.GetInstruction(ip);
if (instruction.OpCode == OpCode.RET) break;
ip += instruction.Size;
to = ip;
}
break;
}
case MethodDetectionMechanism.NextMethod:
case MethodDetectionMechanism.NextMethodInAbi:
{
var next = allMethods.OrderBy(u => u.Offset).Where(u => u.Offset > abiMethod.Offset).FirstOrDefault();
if (next is not null) to = next.Offset - 1;
break;
}
}

// Return method coverage

Expand Down Expand Up @@ -300,7 +369,7 @@ public void Hit(int instructionPointer, Instruction instruction, long gas)
{
// Note: This call is unusual, out of the expected

_coverageData[instructionPointer] = coverage = new CoverageHit(instructionPointer, CoverageHit.DescriptionFromInstruction(instruction), true);
_coverageData[instructionPointer] = coverage = new(instructionPointer, CoverageHit.DescriptionFromInstruction(instruction), true);
}
coverage.Hit(gas);
}
Expand Down
21 changes: 21 additions & 0 deletions src/Neo.SmartContract.Testing/Coverage/MethodDetectionMechanism.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
namespace Neo.SmartContract.Testing.Coverage
{
public enum MethodDetectionMechanism
{
/// <summary>
/// Find RET
/// </summary>
FindRET,

/// <summary>
/// Next method defined in Abi
/// If there are any private method, it probably will return undesired results
/// </summary>
NextMethodInAbi,

/// <summary>
/// It will compute the private methods
/// </summary>
NextMethod
}
}
15 changes: 15 additions & 0 deletions src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ public static class TestExtensions
_ when type == typeof(UInt160) => new UInt160(stackItem.GetSpan().ToArray()),
_ when type == typeof(UInt256) => new UInt256(stackItem.GetSpan().ToArray()),
_ when type == typeof(ECPoint) => ECPoint.FromBytes(stackItem.GetSpan().ToArray(), ECCurve.Secp256r1),
_ when type == typeof(IDictionary<object, object>) && stackItem is Map mp => ToDictionary(mp), // SubItems in StackItem type
_ when type == typeof(Dictionary<object, object>) && stackItem is Map mp => ToDictionary(mp), // SubItems in StackItem type
_ when type == typeof(IList<object>) && stackItem is CompoundType cp => new List<object>(cp.SubItems), // SubItems in StackItem type
_ when type == typeof(List<object>) && stackItem is CompoundType cp => new List<object>(cp.SubItems), // SubItems in StackItem type
_ when typeof(IInteroperable).IsAssignableFrom(type) => CreateInteroperable(stackItem, type),
_ when type.IsArray && stackItem is CompoundType cp => CreateTypeArray(cp.SubItems, type.GetElementType()!),
Expand All @@ -84,6 +87,18 @@ _ when typeof(IInteroperable).IsAssignableFrom(type) => CreateInteroperable(stac
};
}

private static IDictionary<object, object> ToDictionary(Map map)
{
Dictionary<object, object> dictionary = new();

foreach (var entry in map)
{
dictionary.Add(entry.Key, entry.Value);
}

return dictionary;
}

private static object CreateTypeArray(IEnumerable<StackItem> objects, Type elementType)
{
var obj = objects.ToArray();
Expand Down
10 changes: 9 additions & 1 deletion src/Neo.SmartContract.Testing/SmartContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Neo.SmartContract.Testing
{
public class SmartContract
public class SmartContract : IDisposable
{
internal readonly TestEngine Engine;
private readonly Type _contractType;
Expand Down Expand Up @@ -138,5 +138,13 @@ internal void InvokeOnNotify(string eventName, VM.Types.Array state)

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator UInt160(SmartContract value) => value.Hash;

/// <summary>
/// Release mock
/// </summary>
public void Dispose()
{
Engine.ReleaseMock(this);
}
}
}
8 changes: 7 additions & 1 deletion src/Neo.SmartContract.Testing/SmartContractStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ internal SmartContractStorage(SmartContract smartContract, int? contractId = nul
private int GetContractId()
{
// If it was not initialized checking the contract, we need to query the contract id
_contractId ??= _smartContract.Engine.Native.ContractManagement.GetContract(_smartContract.Hash).Id;

if (_contractId is not null) return _contractId.Value;

var state = _smartContract.Engine.Native.ContractManagement.GetContract(_smartContract.Hash)
?? throw new Exception($"The contract {_smartContract.Hash} is not deployed, so it's not possible to get the storage id.");

_contractId = state.Id;
return _contractId.Value;
}

Expand Down
46 changes: 44 additions & 2 deletions src/Neo.SmartContract.Testing/TestEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public class TestEngine
/// </summary>
public bool EnableCoverageCapture { get; set; } = true;

/// <summary>
/// Method detection
/// </summary>
public MethodDetectionMechanism MethodDetection { get; set; } = MethodDetectionMechanism.FindRET;

/// <summary>
/// Validators Address
/// </summary>
Expand Down Expand Up @@ -301,6 +306,11 @@ public T Deploy<T>(NefFile nef, ContractManifest manifest, object? data = null,

var state = Native.ContractManagement.Deploy(nef.ToArray(), Encoding.UTF8.GetBytes(manifest.ToJson().ToString(false)), data);

if (state is null)
{
throw new Exception("Can't get the ContractState");
}

// Mock contract

//UInt160 hash = Helper.GetContractHash(Sender, nef.CheckSum, manifest.Name);
Expand All @@ -313,7 +323,7 @@ public T Deploy<T>(NefFile nef, ContractManifest manifest, object? data = null,
if (EnableCoverageCapture)
{
var coverage = GetCoverage(ret);
coverage?.GenerateMethods(state.Manifest.Abi, state.Script);
coverage?.GenerateMethods(MethodDetection, state);
}

return ret;
Expand Down Expand Up @@ -452,6 +462,38 @@ internal bool TryGetCustomMock(UInt160 hash, string method, int rc, [NotNullWhen
return false;
}

/// <summary>
/// Release custom mock
/// </summary>
/// <param name="contract">Contract</param>
/// <returns>True if a mock was released</returns>
public bool ReleaseMock(SmartContract contract)
{
if (_customMocks.TryGetValue(contract.Hash, out var mocks))
{
// Remove custom mock

var ret = false;

foreach (var entry in mocks.ToArray())
{
if (ReferenceEquals(entry.Value.Contract, contract))
{
if (mocks.Remove(entry.Key)) ret = true;
}
}

if (mocks.Count == 0)
{
_customMocks.Remove(contract.Hash);
}

return ret;
}

return false;
}

/// <summary>
/// Execute raw script
/// </summary>
Expand Down Expand Up @@ -517,7 +559,7 @@ public StackItem Execute(Script script)
var state = Neo.SmartContract.Native.NativeContract.ContractManagement.GetContract(Storage.Snapshot, contract.Hash);
if (state == null) return null;

coveredContract = new(contract.Hash, state.Manifest.Abi, state.Script);
coveredContract = new(MethodDetection, contract.Hash, state);
Coverage[coveredContract.Hash] = coveredContract;
}

Expand Down
3 changes: 2 additions & 1 deletion src/Neo.SmartContract.Testing/TestingApplicationEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ private void RecoverCoverage(Instruction instruction)

var state = Native.NativeContract.ContractManagement.GetContract(Engine.Storage.Snapshot, contractHash);

Engine.Coverage[contractHash] = coveredContract = new(contractHash, state?.Manifest.Abi, InstructionContext.Script);
coveredContract = new(Engine.MethodDetection, contractHash, state);
Engine.Coverage[contractHash] = coveredContract;
}

if (InstructionPointer is null) return;
Expand Down
Loading

0 comments on commit 83600e2

Please sign in to comment.