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

Fix: compilation and coverage issues #921

Merged
merged 22 commits into from
Feb 21, 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
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 @@
/// </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 Expand Up @@ -164,7 +176,7 @@
}
}

if (instruction.Operand.Span.TryGetString(out var str) && Regex.IsMatch(str, @"^[a-zA-Z0-9_]+$"))

Check warning on line 179 in src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs

View workflow job for this annotation

GitHub Actions / Test

Possible null reference argument for parameter 'input' in 'bool Regex.IsMatch(string input, string pattern)'.
{
return ret + $" '{str}'";
}
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
Loading