diff --git a/src/Neo.Compiler.CSharp/Options.cs b/src/Neo.Compiler.CSharp/Options.cs index 204a6b5c5..83a79c608 100644 --- a/src/Neo.Compiler.CSharp/Options.cs +++ b/src/Neo.Compiler.CSharp/Options.cs @@ -21,7 +21,7 @@ public enum GenerateArtifactsKind None, Source, Library, - SourceAndLibrary + All } public string? Output { get; set; } diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index 190cca02a..183bda109 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -193,14 +193,14 @@ private static int ProcessOutputs(Options options, string folder, CompilationCon { var artifact = manifest.GetArtifactsSource(baseName); - if (options.GenerateArtifacts == Options.GenerateArtifactsKind.SourceAndLibrary || options.GenerateArtifacts == Options.GenerateArtifactsKind.Source) + if (options.GenerateArtifacts == Options.GenerateArtifactsKind.All || options.GenerateArtifacts == Options.GenerateArtifactsKind.Source) { path = Path.Combine(outputFolder, $"{baseName}.artifacts.cs"); File.WriteAllText(path, artifact); Console.WriteLine($"Created {path}"); } - if (options.GenerateArtifacts == Options.GenerateArtifactsKind.SourceAndLibrary || options.GenerateArtifacts == Options.GenerateArtifactsKind.Library) + if (options.GenerateArtifacts == Options.GenerateArtifactsKind.All || options.GenerateArtifacts == Options.GenerateArtifactsKind.Library) { try { diff --git a/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs b/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs index c0e743c44..01241fd53 100644 --- a/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs +++ b/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -9,6 +10,8 @@ namespace Neo.SmartContract.Testing.Coverage [DebuggerDisplay("{Name},{PCount}")] public class AbiMethod : IEquatable { + private readonly string _toString; + /// /// Method name /// @@ -23,11 +26,12 @@ public class AbiMethod : IEquatable /// Constructor /// /// Method name - /// Parameters count - public AbiMethod(string name, int pCount) + /// Arguments names + public AbiMethod(string name, string[] argsName) { Name = name; - PCount = pCount; + PCount = argsName.Length; + _toString = name + $"({string.Join(",", argsName)})"; } /// @@ -55,14 +59,14 @@ public static AbiMethod[] CreateFromExpression(Expression expression) return new AbiMethod[] { - new AbiMethod(nameRead, 0), - new AbiMethod(nameWrite, 1) + new AbiMethod(nameRead, Array.Empty()), + new AbiMethod(nameWrite, new string[]{ "value" }) }; } // Only read property - return new AbiMethod[] { new AbiMethod(nameRead, 0) }; + return new AbiMethod[] { new AbiMethod(nameRead, Array.Empty()) }; } } } @@ -73,7 +77,7 @@ public static AbiMethod[] CreateFromExpression(Expression expression) var display = mInfo.GetCustomAttribute(); var name = display is not null ? display.DisplayName : mInfo.Name; - return new AbiMethod[] { new AbiMethod(name, mInfo.GetParameters().Length) }; + return new AbiMethod[] { new AbiMethod(name, mInfo.GetParameters().Select(u => u.Name ?? "arg").ToArray()) }; } } @@ -89,6 +93,6 @@ public override bool Equals(object obj) bool IEquatable.Equals(AbiMethod other) => PCount == other.PCount && Name == other.Name; public override int GetHashCode() => HashCode.Combine(PCount, Name); - public override string ToString() => $"{Name},{PCount}"; + public override string ToString() => _toString; } } diff --git a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs index d419da70c..872299c45 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs @@ -1,9 +1,11 @@ +using Neo.VM; using System; using System.Diagnostics; +using System.Text.RegularExpressions; namespace Neo.SmartContract.Testing.Coverage { - [DebuggerDisplay("Offset:{Offset}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}")] + [DebuggerDisplay("Offset:{Offset}, Description:{Description}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}")] public class CoverageHit { /// @@ -11,6 +13,11 @@ public class CoverageHit /// public int Offset { get; } + /// + /// The instruction description + /// + public string Description { get; } + /// /// The instruction is out of the script /// @@ -45,10 +52,12 @@ public class CoverageHit /// Constructor /// /// Offset + /// Decription /// Out of script - public CoverageHit(int offset, bool outOfScript = false) + public CoverageHit(int offset, string description, bool outOfScript = false) { Offset = offset; + Description = description; OutOfScript = outOfScript; } @@ -104,7 +113,7 @@ public void Hit(CoverageHit value) /// CoverageData public CoverageHit Clone() { - return new CoverageHit(Offset, OutOfScript) + return new CoverageHit(Offset, Description, OutOfScript) { GasMax = GasMax, GasMin = GasMin, @@ -113,13 +122,66 @@ public CoverageHit Clone() }; } + /// + /// Return description from instruction + /// + /// Instruction + /// Description + public static string DescriptionFromInstruction(Instruction instruction) + { + if (instruction.Operand.Length > 0) + { + var ret = instruction.OpCode.ToString() + " 0x" + instruction.Operand.ToArray().ToHexString(); + + switch (instruction.OpCode) + { + case OpCode.JMP: + case OpCode.JMPIF: + case OpCode.JMPIFNOT: + case OpCode.JMPEQ: + case OpCode.JMPNE: + case OpCode.JMPGT: + case OpCode.JMPGE: + case OpCode.JMPLT: + case OpCode.JMPLE: return ret + $" ({instruction.TokenI8})"; + case OpCode.JMP_L: + case OpCode.JMPIF_L: + case OpCode.JMPIFNOT_L: + case OpCode.JMPEQ_L: + case OpCode.JMPNE_L: + case OpCode.JMPGT_L: + case OpCode.JMPGE_L: + case OpCode.JMPLT_L: + case OpCode.JMPLE_L: return ret + $" ({instruction.TokenI32})"; + case OpCode.SYSCALL: + { + if (ApplicationEngine.Services.TryGetValue(instruction.TokenU32, out var syscall)) + { + return ret + $" ('{syscall.Name}')"; + } + + return ret; + } + } + + if (instruction.Operand.Span.TryGetString(out var str) && Regex.IsMatch(str, @"^[a-zA-Z0-9_]+$")) + { + return ret + $" '{str}'"; + } + + return ret; + } + + return instruction.OpCode.ToString(); + } + /// /// String representation /// /// public override string ToString() { - return $"Offset:{Offset}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}"; + return $"Offset:{Offset}, Description:{Description}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}"; } } } diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs index c668a6bf6..a64ffbb62 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs @@ -57,7 +57,7 @@ public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script) while (ip < script.Length) { var instruction = script.GetInstruction(ip); - _coverageData[ip] = new CoverageHit(ip, false); + _coverageData[ip] = new CoverageHit(ip, CoverageHit.DescriptionFromInstruction(instruction), false); ip += instruction.Size; } } @@ -94,7 +94,7 @@ private CoveredMethod CreateMethod(ContractAbi abi, Script script, ContractMetho /// CoveredMethod public CoveredMethod? GetCoverage(string methodName, int pcount) { - return GetCoverage(new AbiMethod(methodName, pcount)); + return Methods.FirstOrDefault(m => m.Method.Name == methodName && m.Method.PCount == pcount); } /// @@ -141,7 +141,16 @@ public void Join(IEnumerable? coverage) /// Dump coverage /// /// Coverage dump - public string Dump() + public string Dump(DumpFormat format = DumpFormat.Console) + { + return Dump(format, Methods); + } + + /// + /// Dump coverage + /// + /// Coverage dump + internal string Dump(DumpFormat format, params CoveredMethod[] methods) { var builder = new StringBuilder(); using var sourceCode = new StringWriter(builder) @@ -149,31 +158,130 @@ public string Dump() NewLine = "\n" }; - var cover = $"{CoveredPercentage:P2}"; - sourceCode.WriteLine($"{Hash} [{cover}]"); + switch (format) + { + case DumpFormat.Console: + { + var cover = $"{CoveredPercentage:P2}"; + sourceCode.WriteLine($"{Hash} [{cover}]"); + + List rows = new(); + var max = new int[] { "Method".Length, "Line ".Length }; - List rows = new(); - var max = new int[] { "Method".Length, "Line ".Length }; + foreach (var method in methods.OrderBy(u => u.Method.Name).OrderByDescending(u => u.CoveredPercentage)) + { + cover = $"{method.CoveredPercentage:P2}"; + rows.Add(new string[] { method.Method.ToString(), cover }); - foreach (var method in Methods.OrderBy(u => u.Method.Name).OrderByDescending(u => u.CoveredPercentage)) - { - cover = $"{method.CoveredPercentage:P2}"; - rows.Add(new string[] { method.Method.ToString(), cover }); + max[0] = Math.Max(method.Method.ToString().Length, max[0]); + max[1] = Math.Max(cover.Length, max[1]); + } - max[0] = Math.Max(method.Method.ToString().Length, max[0]); - max[1] = Math.Max(cover.Length, max[1]); - } + sourceCode.WriteLine($"┌-{"─".PadLeft(max[0], '─')}-┬-{"─".PadLeft(max[1], '─')}-┐"); + sourceCode.WriteLine($"│ {string.Format($"{{0,-{max[0]}}}", "Method", max[0])} │ {string.Format($"{{0,{max[1]}}}", "Line ", max[1])} │"); + sourceCode.WriteLine($"├-{"─".PadLeft(max[0], '─')}-┼-{"─".PadLeft(max[1], '─')}-┤"); - sourceCode.WriteLine($"┌-{"─".PadLeft(max[0], '─')}-┬-{"─".PadLeft(max[1], '─')}-┐"); - sourceCode.WriteLine($"│ {string.Format($"{{0,-{max[0]}}}", "Method", max[0])} │ {string.Format($"{{0,{max[1]}}}", "Line ", max[1])} │"); - sourceCode.WriteLine($"├-{"─".PadLeft(max[0], '─')}-┼-{"─".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])} │"); + } - 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])} │"); + sourceCode.WriteLine($"└-{"─".PadLeft(max[0], '─')}-┴-{"─".PadLeft(max[1], '─')}-┘"); + break; + } + case DumpFormat.Html: + { + sourceCode.WriteLine(@" + + + + +NEF coverage Report + + + +"); + + sourceCode.WriteLine($@" +
+
{Hash}
+
{CoveredPercentage:P2}
+
+
+
+"); + + foreach (var method in methods.OrderBy(u => u.Method.Name).OrderByDescending(u => u.CoveredPercentage)) + { + var kind = "low"; + if (method.CoveredPercentage > 0.7) kind = "medium"; + if (method.CoveredPercentage > 0.8) kind = "high"; + + sourceCode.WriteLine($@" +
+
{method.Method}
+
{method.CoveredPercentage:P2}
+
+
+"); + sourceCode.WriteLine($@"
"); + + foreach (var hit in method.Coverage) + { + var noHit = hit.Hits == 0 ? "no-" : ""; + var icon = hit.Hits == 0 ? "✘" : "✔"; + + sourceCode.WriteLine($@"
{icon}{hit.Hits} Hits{hit.Description}
"); + } + + sourceCode.WriteLine($@"
+"); + } + + sourceCode.WriteLine(@" +
+ + + + +"); + break; + } } - - sourceCode.WriteLine($"└-{"─".PadLeft(max[0], '─')}-┴-{"─".PadLeft(max[1], '─')}-┘"); return builder.ToString(); } @@ -182,8 +290,9 @@ public string Dump() /// Hit /// /// Instruction pointer + /// Instruction /// Gas - public void Hit(int instructionPointer, long gas) + public void Hit(int instructionPointer, Instruction instruction, long gas) { lock (_coverageData) { @@ -191,7 +300,7 @@ public void Hit(int instructionPointer, long gas) { // Note: This call is unusual, out of the expected - _coverageData[instructionPointer] = coverage = new CoverageHit(instructionPointer, true); + _coverageData[instructionPointer] = coverage = new CoverageHit(instructionPointer, CoverageHit.DescriptionFromInstruction(instruction), true); } coverage.Hit(gas); } diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs index 15a8b4276..0ed2aa80f 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs @@ -1,6 +1,7 @@ using Neo.SmartContract.Manifest; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Neo.SmartContract.Testing.Coverage { @@ -41,11 +42,17 @@ public class CoveredMethod : CoverageBase public CoveredMethod(CoveredContract contract, ContractMethodDescriptor method, int methodLength) { Contract = contract; - Method = new AbiMethod(method.Name, method.Parameters.Length); Offset = method.Offset; MethodLength = methodLength; + Method = new AbiMethod(method.Name, method.Parameters.Select(u => u.Name).ToArray()); } + /// + /// Dump coverage + /// + /// Coverage dump + public string Dump(DumpFormat format = DumpFormat.Console) => Contract.Dump(format, this); + public override string ToString() => Method.ToString(); } } diff --git a/src/Neo.SmartContract.Testing/Coverage/DumpFormat.cs b/src/Neo.SmartContract.Testing/Coverage/DumpFormat.cs new file mode 100644 index 000000000..94f0390fe --- /dev/null +++ b/src/Neo.SmartContract.Testing/Coverage/DumpFormat.cs @@ -0,0 +1,15 @@ +namespace Neo.SmartContract.Testing.Coverage +{ + public enum DumpFormat : byte + { + /// + /// Console format + /// + Console, + + /// + /// HTML Format + /// + Html + } +} diff --git a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs index 71f1c8a9c..b03300d64 100644 --- a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs +++ b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs @@ -46,6 +46,7 @@ public static class TestExtensions return type switch { + _ when type == typeof(object) => stackItem, _ when type == typeof(string) => Utility.StrictUTF8.GetString(stackItem.GetSpan()), _ when type == typeof(byte[]) => stackItem.GetSpan().ToArray(), diff --git a/src/Neo.SmartContract.Testing/README.md b/src/Neo.SmartContract.Testing/README.md index fd900ba9e..a413ec068 100644 --- a/src/Neo.SmartContract.Testing/README.md +++ b/src/Neo.SmartContract.Testing/README.md @@ -17,6 +17,9 @@ The **Neo.SmartContract.Testing** project is designed to facilitate the developm - [SmartContractStorage](#smartcontractstorage) - [Methods](#methods) - [Example of use](#example-of-use) +- [Checkpoints](#checkpoints) + - [Methods](#methods) + - [Example of use](#example-of-use) - [Custom mocks](#custom-mocks) - [Example of use](#example-of-use) - [Forging signatures](#forging-signatures) @@ -33,9 +36,10 @@ The **Neo.SmartContract.Testing** project is designed to facilitate the developm The process of generating the artifacts, or the source code necessary to interact with the contract, is extremely simple. There are two main ways to do it: -1. Using the `ContractManifest` of a contract, the necessary source code to interact with the contract can be generated by calling the `GetArtifactsSource` method available in the `Neo.SmartContract.Testing.Extensions` namespace, we will only have to specify the name of our resulting class, which will usually be the same as the one existing in the `Name` field of the manifest. +1. Using the `ContractManifest` of a contract, the necessary source code to interact with the contract can be generated by calling the `GetArtifactsSource` method available in the `Neo.SmartContract.Testing.Extensions.ArtifactExtensions` class, we will only have to specify the name of our resulting class, which will usually be the same as the one existing in the `Name` field of the manifest. + +2. Through the Neo C# compiler, automatically when compiling a contract in C#, the necessary source code to interact with the contract is generated. This is available in the same path as the generated .nef file, and its extension are `.artifacts.cs` and `.artifacts.dll`. Using the `--generate-artifacts` argument in `Neo.Compiler.CSharp` followed by the type of artifacts, with the options being: `none`, `source`, `library`, and `all`. -2. Through the Neo C# compiler, automatically when compiling a contract in C#, the necessary source code to interact with the contract is generated. This is available in the same path as the generated .nef file, and its extension are `.artifacts.cs` and `.artifacts.dll`. ##### Example of use @@ -72,15 +76,13 @@ The publicly exposed read-only properties are as follows: - **CommitteeAddress**: Returns the address of the current chain's committee. - **Transaction**: Defines the transaction that will be used as `ScriptContainer` for the neo virtual machine, by default it updates the script of the same as calls are composed and executed, and the `Signers` will be used as validators for the `CheckWitness`, regardless of whether the signature is correct or not, so if you want to test with different wallets or scopes, you do not need to sign the transaction correctly, just set the desired signers. - **CurrentBlock**: Defaults to `Genesis` for the defined `ProtocolSettings`, but the height has been incremented by 1 to avoid issues related to the generation of gas from native contracts. -- **EnableCoverageCapture**: Enables or disables the coverage capture. - -For initialize, we have: - -- **Storage**: Abstracts access to storage, allowing for easy `Snapshots` as well as reverting them. It can only be set during the initialization of the class, and allows access to the storage of contracts, as well as manually altering their state. And for read and write, we have: +- **Storage**: Abstracts access to storage, allowing for easy `Snapshots` as well as reverting them. Allows access to the storage of contracts, as well as manually altering their state. It's worth noting that a storage class is provided, which allows for reading the storage from an RPC endpoint. The class in question is named `RpcStore` and is available in the namespace `Neo.SmartContract.Testing.Storage.Rpc`. + - **Gas**: Sets the gas execution limit for contract calls. Sets the `NetworkFee` of the `Transaction` object. +- **EnableCoverageCapture**: Enables or disables the coverage capture. #### Methods @@ -91,6 +93,8 @@ It has four methods: - **FromHash(hash, customMocks, checkExistence)**: Creates an instance without needing a `NefFile` or `Manifest`, only requiring the contract's hash. It does not consider whether the contract exists on the chain unless `checkExistence` is set to `true`. - **SetTransactionSigners(signers)**: Set the `Signer` of the `Transaction`. - **GetNewSigner(scope)**: A static method that provides us with a random `Signer` signed by default by `CalledByEntry`. +- **GetDeployHash(nef, manifest)**: Gets the hash that will result from deploying a contract with the defined `NefFile` and `Manifest`. + #### Example of use @@ -138,7 +142,7 @@ Avoids dealing with prefixes foreign to the internal behavior of the storage, fo #### Methods -Mainly exposes the methods `Export`, `Import`, `Contains`, `Get`, `Put`, and `Remove`, all of them responsible for reading and manipulating the contract's information. +Mainly exposes the methods `Import`, `Export`, `Contains`, `Get`, `Put`, and `Remove`, all of them responsible for reading and manipulating the contract's information. #### Example of use @@ -164,6 +168,50 @@ engine.Native.NEO.Storage.Put(registerPricePrefix, BigInteger.MinusOne); Assert.AreEqual(BigInteger.MinusOne, engine.Native.NEO.RegisterPrice); ``` +### Checkpoints + +Storage checkpoints can be created, allowing for a return to specific moments in the execution. This can be achieved with checkpoints. + +To create a checkpoint, simply call `Checkpoint()` from a `EngineStorage` class or from our `TestEngine`. + +#### Methods + +It has the following methods: + +- **Restore(snapshot)**: This method can also be called from an `EngineStorage` or from our `TestEngine` class. It is used to restore the storage to a specified checkpoint. +- **ToArray()**: Exports the checkpoint to a `byte[]`. +- **Write(stream)**: Writes the checkpoint to a `Stream`. + +#### Example of use + +```csharp +// Create a new test engine with native contracts already initialized + +var engine = new TestEngine(true); + +// Check that all it works + +Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + +// Create checkpoint + +var checkpoint = engine.Storage.Checkpoint(); + +// Create new storage, and restore the checkpoint on it + +var storage = new EngineStorage(new MemoryStore()); +checkpoint.Restore(storage.Snapshot); + +// Create new test engine without initialize +// and set the storage to the restored one + +engine = new TestEngine(false) { Storage = storage }; + +// Ensure that all works + +Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); +``` + ### Custom mocks Custom mocks allow redirecting certain calls to smart contracts so that instead of calling the underlying contract, the logic is redirected to a method in .NET, allowing the developer to test in complex environments without significant issues. @@ -337,7 +385,38 @@ Assert.AreEqual(3, methodCovered?.TotalInstructions); Assert.AreEqual(3, methodCovered?.CoveredInstructions); ``` -Keep in mind that the coverage is at the instruction level. +Additionally, it's important to highlight that both method and contract coverages have a `Dump` method, through which one can obtain a text or HTML representation of the coverage. +You might be interested in adding a unit test that checks the coverage at the end of execution, you can do it as shown below: + +```csharp +[TestClass] +public class CoverageContractTests +{ + /// + /// Required coverage to be success + /// + public static float RequiredCoverage { get; set; } = 1F; + + [AssemblyCleanup] + public static void EnsureCoverage() + { + // Join here all of your Coverage sources + + var coverage = Nep17ContractTests.Coverage; + coverage?.Join(OwnerContractTests.Coverage); + + // Ennsure that the coverage is more than X% at the end of the tests + + Assert.IsNotNull(coverage); + Console.WriteLine(coverage.Dump()); + + File.WriteAllText("coverage.html", coverage.Dump(Testing.Coverage.DumpFormat.Html)); + Assert.IsTrue(coverage.CoveredPercentage >= RequiredCoverage, $"Coverage is less than {RequiredCoverage:P2}"); + } +} +``` + +Keep in mind that the coverage is at the instruction level. ### Known limitations diff --git a/src/Neo.SmartContract.Testing/SmartContract.cs b/src/Neo.SmartContract.Testing/SmartContract.cs index 042173802..ed957e3d5 100644 --- a/src/Neo.SmartContract.Testing/SmartContract.cs +++ b/src/Neo.SmartContract.Testing/SmartContract.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; namespace Neo.SmartContract.Testing { @@ -134,5 +135,8 @@ internal void InvokeOnNotify(string eventName, VM.Types.Array state) handler.Method.Invoke(handler.Target, args); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator UInt160(SmartContract value) => value.Hash; } } diff --git a/src/Neo.SmartContract.Testing/Storage/EngineCheckpoint.cs b/src/Neo.SmartContract.Testing/Storage/EngineCheckpoint.cs new file mode 100644 index 000000000..770851d52 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Storage/EngineCheckpoint.cs @@ -0,0 +1,113 @@ +using Neo.IO; +using Neo.Persistence; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Neo.SmartContract.Testing.Storage +{ + public class EngineCheckpoint + { + /// + /// Data + /// + public (byte[] key, byte[] value)[] Data { get; } + + /// + /// Constructor + /// + /// Snapshot + public EngineCheckpoint(SnapshotCache snapshot) + { + var list = new List<(byte[], byte[])>(); + + foreach (var entry in snapshot.Seek(Array.Empty(), SeekDirection.Forward)) + { + list.Add((entry.Key.ToArray(), entry.Value.ToArray())); + } + + Data = list.ToArray(); + } + + /// + /// Constructor + /// + /// Stream + public EngineCheckpoint(Stream stream) + { + var list = new List<(byte[], byte[])>(); + var buffer = new byte[sizeof(int)]; + + while (stream.Read(buffer) == sizeof(int)) + { + var length = BinaryPrimitives.ReadInt32LittleEndian(buffer); + var key = new byte[length]; + + if (stream.Read(key) != length) break; + if (stream.Read(buffer) != sizeof(int)) break; + + length = BinaryPrimitives.ReadInt32LittleEndian(buffer); + var data = new byte[length]; + + if (stream.Read(data) != length) break; + + list.Add((key, data)); + } + + Data = list.ToArray(); + } + + /// + /// Restore + /// + /// Snapshot + public void Restore(SnapshotCache snapshot) + { + // Clean snapshot + + foreach (var entry in snapshot.Seek(Array.Empty(), SeekDirection.Forward).ToArray()) + { + snapshot.Delete(entry.Key); + } + + // Restore + + foreach (var entry in Data) + { + snapshot.Add(new StorageKey(entry.key), new StorageItem(entry.value)); + } + } + + /// + /// To Array + /// + /// binary data + public byte[] ToArray() + { + using var ms = new MemoryStream(); + Write(ms); + return ms.ToArray(); + } + + /// + /// Write to Stream + /// + public void Write(Stream stream) + { + var buffer = new byte[sizeof(int)]; + + foreach (var entry in Data) + { + BinaryPrimitives.WriteInt32LittleEndian(buffer, entry.key.Length); + stream.Write(buffer); + stream.Write(entry.key); + + BinaryPrimitives.WriteInt32LittleEndian(buffer, entry.value.Length); + stream.Write(buffer); + stream.Write(entry.value); + } + } + } +} diff --git a/src/Neo.SmartContract.Testing/TestStorage.cs b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs similarity index 87% rename from src/Neo.SmartContract.Testing/TestStorage.cs rename to src/Neo.SmartContract.Testing/Storage/EngineStorage.cs index d7b7fe8d9..8c4500db7 100644 --- a/src/Neo.SmartContract.Testing/TestStorage.cs +++ b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs @@ -4,12 +4,12 @@ using System.Buffers.Binary; using System.Linq; -namespace Neo.SmartContract.Testing +namespace Neo.SmartContract.Testing.Storage { /// /// TestStorage centralizes the storage management of our TestEngine /// - public class TestStorage + public class EngineStorage { // Key to check if native contracts are initialized, by default: Neo.votersCountPrefix private static readonly StorageKey _initKey = new() { Id = Native.NativeContract.NEO.Id, Key = new byte[] { 1 } }; @@ -32,8 +32,8 @@ public class TestStorage /// /// Constructor /// - /// Store - public TestStorage(IStore store) + /// Store + public EngineStorage(IStore store) { Store = store; Snapshot = new SnapshotCache(Store.GetSnapshot()); @@ -56,6 +56,21 @@ public void Rollback() Snapshot = new SnapshotCache(Store.GetSnapshot()); } + /// + /// Get storage checkpoint + /// + /// EngineCheckpoint + public EngineCheckpoint Checkpoint() => new(Snapshot); + + /// + /// Restore + /// + /// Checkpoint + public void Restore(EngineCheckpoint checkpoint) + { + checkpoint.Restore(Snapshot); + } + /// /// Import data from json, expected data (in base64): /// - "key" : "value" diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcSnapshot.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcSnapshot.cs new file mode 100644 index 000000000..ee3c101bc --- /dev/null +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcSnapshot.cs @@ -0,0 +1,64 @@ +using Neo.Persistence; +using System; +using System.Collections.Generic; + +namespace Neo.SmartContract.Testing.Storage.Rpc; + +internal class RpcSnapshot : ISnapshot +{ + /// + /// Return true if the storage has changes + /// + public bool IsDirty { get; private set; } = false; + + /// + /// Store + /// + public RpcStore Store { get; } + + /// + /// Constructor + /// + /// Store + /// Inner data + public RpcSnapshot(RpcStore store) + { + Store = store; + } + + public void Commit() + { + if (IsDirty) + { + throw new NotImplementedException(); + } + } + + public void Delete(byte[] key) + { + IsDirty = true; + } + + + public void Put(byte[] key, byte[] value) + { + IsDirty = true; + } + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] keyOrPrefix, SeekDirection direction = SeekDirection.Forward) + { + return Store.Seek(keyOrPrefix, direction); + } + + public byte[]? TryGet(byte[] key) + { + return Store.TryGet(key); + } + + public bool Contains(byte[] key) + { + return TryGet(key) != null; + } + + public void Dispose() { } +} diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs new file mode 100644 index 000000000..3c0bf8446 --- /dev/null +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -0,0 +1,173 @@ +using Neo.IO; +using Neo.Persistence; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading; + +namespace Neo.SmartContract.Testing.Storage.Rpc; + +public class RpcStore : IStore +{ + private int _id = 0; + + /// + /// Url + /// + public Uri Url { get; set; } + + /// + /// Constructor + /// + /// Url + public RpcStore(Uri url) + { + Url = url; + } + + /// + /// Constructor + /// + /// Url + public RpcStore(string url) : this(new Uri(url)) { } + + public void Delete(byte[] key) => throw new NotImplementedException(); + public void Put(byte[] key, byte[] value) => throw new NotImplementedException(); + public ISnapshot GetSnapshot() => new RpcSnapshot(this); + public bool Contains(byte[] key) => TryGet(key) != null; + public void Dispose() { } + + #region Rpc calls + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] key, SeekDirection direction) + { + if (direction is SeekDirection.Backward) + { + // Not implemented in RPC, we will query all the storage from the contract, and do it manually + // it could return wrong results if we want to get data between contracts + + var prefix = key.Take(4).ToArray(); + ConcurrentDictionary data = new(); + + // We ask for 5 bytes because the minimum prefix is one byte + + foreach (var entry in Seek(key.Take(key.Length == 4 ? 4 : 5).ToArray(), SeekDirection.Forward)) + { + data.TryAdd(entry.Key, entry.Value); + } + + foreach (var entry in new MemorySnapshot(data).Seek(key, direction)) + { + yield return (entry.Key, entry.Value); + } + + yield break; + } + + var skey = new StorageKey(key); + var start = 0; + + while (true) + { + var requestBody = new + { + jsonrpc = "2.0", + method = "findstorage", + @params = new string[] { skey.Id.ToString(), Convert.ToBase64String(skey.Key.ToArray()), start.ToString() }, + id = _id = Interlocked.Increment(ref _id), + }; + + using var httpClient = new HttpClient(); + var requestContent = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json"); + var response = httpClient.PostAsync(Url, requestContent).GetAwaiter().GetResult().EnsureSuccessStatusCode(); + + JObject jo = JObject.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + + if (jo["result"]?.Value() is JObject result && result["results"]?.Value() is JArray results) + { + // iterate page + + var prefix = skey.ToArray().Take(4); + + foreach (JObject r in results) + { + if (r["key"]?.Value() is string jkey && + r["value"]?.Value() is string kvalue) + { + yield return (prefix.Concat(Convert.FromBase64String(jkey)).ToArray(), Convert.FromBase64String(kvalue)); + } + } + + if (result["truncated"]?.Value() == true && + result["next"]?.Value() is int next) + { + start = next; + } + else + { + yield break; + } + } + else + { + // {"jsonrpc":"2.0","id":3,"error":{"code":-100,"message":"Unknown storage","data":" ... + + if (jo["error"]?.Value() is JObject error && + error["code"]?.Value() is int errorCode && + errorCode == -100) + { + yield break; + } + + throw new Exception(); + } + } + + throw new Exception(); + } + + public byte[]? TryGet(byte[] key) + { + var skey = new StorageKey(key); + var requestBody = new + { + jsonrpc = "2.0", + method = "getstorage", + @params = new string[] { skey.Id.ToString(), Convert.ToBase64String(skey.Key.ToArray()) }, + id = _id = Interlocked.Increment(ref _id), + }; + + using var httpClient = new HttpClient(); + var requestContent = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json"); + var response = httpClient.PostAsync(Url, requestContent).GetAwaiter().GetResult().EnsureSuccessStatusCode(); + + JObject jo = JObject.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + + if (jo["result"]?.Value() is string result) + { + // {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"Aw==\"} + + return Convert.FromBase64String(result); + } + else + { + // {"jsonrpc":"2.0","id":3,"error":{"code":-100,"message":"Unknown storage","data":" ... + + if (jo["error"]?.Value() is JObject error && + error["code"]?.Value() is int errorCode && + errorCode == -100) + { + return null; + } + + throw new Exception(); + } + } + + #endregion +} diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 0d0fe4a58..7a0843bee 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -6,6 +6,7 @@ using Neo.SmartContract.Manifest; using Neo.SmartContract.Testing.Coverage; using Neo.SmartContract.Testing.Extensions; +using Neo.SmartContract.Testing.Storage; using Neo.VM; using Neo.VM.Types; using System; @@ -71,7 +72,7 @@ public class TestEngine /// /// Storage /// - public TestStorage Storage { get; set; } = new TestStorage(new MemoryStore()); + public EngineStorage Storage { get; set; } = new EngineStorage(new MemoryStore()); /// /// Protocol Settings @@ -185,13 +186,13 @@ public TestEngine(ProtocolSettings settings, bool initializeNativeContracts = tr NetworkFee = ApplicationEngine.TestModeGas, Signers = new Signer[] { - new Signer() + new() { // ValidatorsAddress Account = validatorsScript.ToScriptHash(), Scopes = WitnessScope.Global }, - new Signer() + new() { // CommitteeAddress Account = committeeScript.ToScriptHash(), @@ -233,6 +234,22 @@ internal void ApplicationEngineLog(object? sender, LogEventArgs e) #endregion + #region Checkpoints + + /// + /// Get storage checkpoint + /// + /// EngineCheckpoint + public EngineCheckpoint Checkpoint() => Storage.Checkpoint(); + + /// + /// Restore + /// + /// Checkpoint + public void Restore(EngineCheckpoint checkpoint) => Storage.Restore(checkpoint); + + #endregion + /// /// Get deploy hash /// @@ -241,8 +258,18 @@ internal void ApplicationEngineLog(object? sender, LogEventArgs e) /// Contract hash public UInt160 GetDeployHash(byte[] nef, string manifest) { - return Helper.GetContractHash(Sender, - nef.AsSerializable().CheckSum, ContractManifest.Parse(manifest).Name); + return GetDeployHash(nef.AsSerializable(), ContractManifest.Parse(manifest)); + } + + /// + /// Get deploy hash + /// + /// Nef + /// Manifest + /// Contract hash + public UInt160 GetDeployHash(NefFile nef, ContractManifest manifest) + { + return Helper.GetContractHash(Sender, nef.CheckSum, manifest.Name); } /// @@ -444,6 +471,12 @@ public StackItem Execute(Script script) engine.LoadScript(script); + // Clean events, if we Execute inside and execute + // becaus it's a mock, we can register twice + + ApplicationEngine.Log -= ApplicationEngineLog; + ApplicationEngine.Notify -= ApplicationEngineNotify; + // Attach to static event ApplicationEngine.Log += ApplicationEngineLog; @@ -594,6 +627,10 @@ public static Signer GetNewSigner(WitnessScope scope = WitnessScope.CalledByEntr var data = new byte[UInt160.Length]; rand.NextBytes(data); + // Ensure that if we convert to BigInteger this value will work + + if (data[0] == 0) data[0] = 1; + return new Signer() { Account = new UInt160(data), diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 3ce77e5d5..71ac969d5 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -2,6 +2,7 @@ using Neo.Persistence; using Neo.SmartContract.Testing.Extensions; using Neo.VM; +using Neo.VM.Types; using System; namespace Neo.SmartContract.Testing @@ -11,6 +12,7 @@ namespace Neo.SmartContract.Testing /// internal class TestingApplicationEngine : ApplicationEngine { + private Instruction? PreInstruction; private ExecutionContext? InstructionContext; private int? InstructionPointer; private long PreExecuteInstructionGasConsumed; @@ -32,6 +34,7 @@ protected override void PreExecuteInstruction(Instruction instruction) if (Engine.EnableCoverageCapture) { + PreInstruction = instruction; PreExecuteInstructionGasConsumed = GasConsumed; InstructionContext = CurrentContext; InstructionPointer = InstructionContext?.InstructionPointer; @@ -42,10 +45,25 @@ protected override void PreExecuteInstruction(Instruction instruction) base.PreExecuteInstruction(instruction); } + protected override void OnFault(Exception ex) + { + base.OnFault(ex); + + if (PreInstruction is not null) + { + // PostExecuteInstruction is not executed onFault + RecoverCoverage(PreInstruction); + } + } + protected override void PostExecuteInstruction(Instruction instruction) { base.PostExecuteInstruction(instruction); + RecoverCoverage(instruction); + } + private void RecoverCoverage(Instruction instruction) + { // We need the script to know the offset if (InstructionContext is null) return; @@ -65,7 +83,11 @@ protected override void PostExecuteInstruction(Instruction instruction) if (InstructionPointer is null) return; - coveredContract.Hit(InstructionPointer.Value, GasConsumed - PreExecuteInstructionGasConsumed); + coveredContract.Hit(InstructionPointer.Value, instruction, GasConsumed - PreExecuteInstructionGasConsumed); + + PreInstruction = null; + InstructionContext = null; + InstructionPointer = null; } protected override void OnSysCall(InteropDescriptor descriptor) @@ -118,6 +140,8 @@ protected override void OnSysCall(InteropDescriptor descriptor) var returnValue = customMock.Method.Invoke(customMock.Contract, parameters); if (hasReturnValue) Push(Convert(returnValue)); + else + Push(StackItem.Null); return; } diff --git a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs index 3b0fa94e6..a2981bf70 100644 --- a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs +++ b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs @@ -1,6 +1,10 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.IO; +using Neo.SmartContract.Manifest; using Neo.SmartContract.Testing.InvalidTypes; using Neo.VM; +using Neo.VM.Types; using System.Collections.Generic; using System.Numerics; @@ -9,6 +13,13 @@ namespace Neo.SmartContract.Testing.TestingStandards; public class Nep17Tests : TestBase where T : SmartContract, INep17Standard { + public abstract class onNEP17PaymentContract : SmartContract + { + protected onNEP17PaymentContract(SmartContractInitialize initialize) : base(initialize) { } + + public abstract void onNEP17Payment(UInt160? from, BigInteger? amount, object? data = null); + } + /// /// Expected total supply /// @@ -150,6 +161,65 @@ public virtual void TestTransfer() Assert.AreEqual(0, Contract.BalanceOf(Bob.Account)); Assert.AreEqual(initialSupply, Contract.TotalSupply); AssertTransferEvent(Bob.Account, Alice.Account, 3); + + // Test onNEP17Payment with a mock + // We create a mock contract using the current nef and manifest + // Only we need to create the manifest method, then it will be redirected + + ContractManifest manifest = ContractManifest.Parse(Manifest); + manifest.Abi.Methods = new ContractMethodDescriptor[] + { + new () + { + Name = "onNEP17Payment", + ReturnType = ContractParameterType.Void, + Safe = false, + Parameters = new ContractParameterDefinition[] + { + new() { Name = "a", Type = ContractParameterType.Hash160 }, + new() { Name = "b", Type = ContractParameterType.Integer }, + new() { Name = "c", Type = ContractParameterType.Any } + } + } + }; + + // Deploy dummy contract + + UInt160? calledFrom = null; + BigInteger? calledAmount = null; + byte[]? calledData = null; + + var mock = Engine.Deploy(NefFile, manifest.ToJson().ToString(), null, m => + { + m + .Setup(s => s.onNEP17Payment(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback(new InvocationAction((i) => + { + calledFrom = i.Arguments[0] as UInt160; + calledAmount = (BigInteger)i.Arguments[1]; + calledData = (i.Arguments[2] as ByteString)!.GetSpan().ToArray(); + + // Ensure the event was called + + var me = new UInt160(calledData); + AssertTransferEvent(Alice.Account, me, calledAmount); + })); + }); + + // Ensure that was called + + Engine.SetTransactionSigners(Alice); + Assert.IsTrue(Contract.Transfer(Alice.Account, mock.Hash, 3, mock.Hash.ToArray())); + + Assert.AreEqual(Alice.Account, calledFrom); + Assert.AreEqual(mock.Hash, new UInt160(calledData)); + Assert.AreEqual(3, calledAmount); + + // Return the money back + + Engine.SetTransactionSigners(mock); + Assert.IsTrue(Contract.Transfer(mock.Hash, calledFrom, calledAmount)); + AssertTransferEvent(mock.Hash, Alice.Account, calledAmount); } #endregion diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs index f242a2a8b..606af9e30 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs @@ -8,20 +8,23 @@ public class CoverageContractTests /// /// Required coverage to be success /// - public static float RequiredCoverage { get; set; } = 0.95F; + public static float RequiredCoverage { get; set; } = 1F; [AssemblyCleanup] public static void EnsureCoverage() { - // Ennsure that the coverage is more than X% at the end of the tests + // Join here all of your Coverage sources var coverage = Nep17ContractTests.Coverage; coverage?.Join(OwnerContractTests.Coverage); - Assert.IsNotNull(coverage); + // Ensure that the coverage is more than X% at the end of the tests + Assert.IsNotNull(coverage); Console.WriteLine(coverage.Dump()); - Assert.IsTrue(coverage.CoveredPercentage > RequiredCoverage, $"Coverage is less than {RequiredCoverage:P2}"); + + File.WriteAllText("coverage.html", coverage.Dump(Testing.Coverage.DumpFormat.Html)); + Assert.IsTrue(coverage.CoveredPercentage >= RequiredCoverage, $"Coverage is less than {RequiredCoverage:P2}"); } } } diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs index 559e1b206..b5c2d2b53 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.SmartContract.Testing; +using Neo.SmartContract.Testing.InvalidTypes; using Neo.SmartContract.Testing.TestingStandards; using Neo.VM; using System.Numerics; @@ -159,6 +160,11 @@ public void TestDeployWithOwner() Engine.SetTransactionSigners(Bob); + // Try with invalid owners + + Assert.ThrowsException(() => Engine.Deploy(NefFile, Manifest, UInt160.Zero)); + Assert.ThrowsException(() => Engine.Deploy(NefFile, Manifest, InvalidUInt160.Invalid)); + // Test SetOwner notification UInt160? previousOwnerRaised = null; diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs index f69001b95..b42cc142b 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.SmartContract.Testing; using Neo.SmartContract.Testing.TestingStandards; +using Neo.VM; namespace Neo.SmartContract.Template.UnitTests.templates.neocontractnep17 { @@ -19,5 +20,17 @@ public OwnerContractTests() : "templates/neocontractnep17/Artifacts/Nep17Contract.manifest.json" ) { } + + [TestMethod] + public override void TestSetGetOwner() + { + base.TestSetGetOwner(); + + // Test throw if was stored an invalid owner + // Technically not possible, but raise 100% coverage + + Contract.Storage.Put(new byte[] { 0xff }, 123); + Assert.ThrowsException(() => Contract.Owner); + } } } diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs index 4f6296337..3af364092 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs @@ -19,30 +19,39 @@ public void TestDump() Assert.AreEqual(@" 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5 [5.26%] -┌-────────────────────────-┬-───────-┐ -│ Method │ Line │ -├-────────────────────────-┼-───────-┤ -│ totalSupply,0 │ 100.00% │ -│ balanceOf,1 │ 0.00% │ -│ decimals,0 │ 0.00% │ -│ getAccountState,1 │ 0.00% │ -│ getAllCandidates,0 │ 0.00% │ -│ getCandidates,0 │ 0.00% │ -│ getCandidateVote,1 │ 0.00% │ -│ getCommittee,0 │ 0.00% │ -│ getGasPerBlock,0 │ 0.00% │ -│ getNextBlockValidators,0 │ 0.00% │ -│ getRegisterPrice,0 │ 0.00% │ -│ registerCandidate,1 │ 0.00% │ -│ setGasPerBlock,1 │ 0.00% │ -│ setRegisterPrice,1 │ 0.00% │ -│ symbol,0 │ 0.00% │ -│ transfer,4 │ 0.00% │ -│ unclaimedGas,2 │ 0.00% │ -│ unregisterCandidate,1 │ 0.00% │ -│ vote,2 │ 0.00% │ -└-────────────────────────-┴-───────-┘ +┌-───────────────────────────────-┬-───────-┐ +│ Method │ Line │ +├-───────────────────────────────-┼-───────-┤ +│ totalSupply() │ 100.00% │ +│ balanceOf(account) │ 0.00% │ +│ decimals() │ 0.00% │ +│ getAccountState(account) │ 0.00% │ +│ getAllCandidates() │ 0.00% │ +│ getCandidates() │ 0.00% │ +│ getCandidateVote(pubKey) │ 0.00% │ +│ getCommittee() │ 0.00% │ +│ getGasPerBlock() │ 0.00% │ +│ getNextBlockValidators() │ 0.00% │ +│ getRegisterPrice() │ 0.00% │ +│ registerCandidate(pubkey) │ 0.00% │ +│ setGasPerBlock(gasPerBlock) │ 0.00% │ +│ setRegisterPrice(registerPrice) │ 0.00% │ +│ symbol() │ 0.00% │ +│ transfer(from,to,amount,data) │ 0.00% │ +│ unclaimedGas(account,end) │ 0.00% │ +│ unregisterCandidate(pubkey) │ 0.00% │ +│ vote(account,voteTo) │ 0.00% │ +└-───────────────────────────────-┴-───────-┘ ".Trim(), engine.GetCoverage(engine.Native.NEO)?.Dump().Trim()); + + Assert.AreEqual(@" +0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5 [5.26%] +┌-─────────────-┬-───────-┐ +│ Method │ Line │ +├-─────────────-┼-───────-┤ +│ totalSupply() │ 100.00% │ +└-─────────────-┴-───────-┘ +".Trim(), (engine.Native.NEO.GetCoverage(o => o.TotalSupply) as CoveredMethod)?.Dump().Trim()); } [TestMethod] @@ -186,9 +195,10 @@ public void TestCoverageByExtension() [TestMethod] public void TestHits() { - var coverage = new CoverageHit(0); + var coverage = new CoverageHit(0, "test"); Assert.AreEqual(0, coverage.Hits); + Assert.AreEqual("test", coverage.Description); Assert.AreEqual(0, coverage.GasAvg); Assert.AreEqual(0, coverage.GasMax); Assert.AreEqual(0, coverage.GasMin); diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs index b3c0cfef1..f8186d8b3 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs @@ -3,7 +3,7 @@ using Neo.SmartContract.Manifest; using Neo.SmartContract.Testing.Extensions; -namespace Neo.SmartContract.TestEngine.UnitTests.Extensions +namespace Neo.SmartContract.Testing.UnitTests.Extensions { [TestClass] public class ArtifactExtensionsTests diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs new file mode 100644 index 000000000..a18535ed9 --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs @@ -0,0 +1,47 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Neo.Cryptography.ECC; +using Neo.SmartContract.Testing.Storage; +using Neo.SmartContract.Testing.Storage.Rpc; +using System.Numerics; + +namespace Neo.SmartContract.Testing.UnitTests.Storage +{ + [TestClass] + public class RpcStoreTests + { + public abstract class DummyContract : SmartContract + { + public abstract BigInteger GetCandidateVote(ECPoint point); + protected DummyContract(SmartContractInitialize initialize) : base(initialize) { } + } + + [TestMethod] + public void TestRpcStore() + { + var engine = new TestEngine(false) + { + Storage = new EngineStorage(new RpcStore("http://seed2t5.neo.org:20332")) + }; + + // check network values + + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + Assert.IsTrue(engine.Native.Ledger.CurrentIndex > 3_510_270); + + // check with Seek (RPC doesn't support Backward, it could be slow) + + Assert.IsTrue(engine.Native.NEO.GasPerBlock == 500000000); + + // check deploy + + var node = ECPoint.Parse("03009b7540e10f2562e5fd8fac9eaec25166a58b26e412348ff5a86927bfac22a2", ECCurve.Secp256r1); + var state = engine.Native.ContractManagement.GetContract(engine.Native.NEO.Hash); + var contract = engine.Deploy(state.Nef, state.Manifest, null, + c => c.Setup(s => s.GetCandidateVote(It.IsAny())).Returns(() => engine.Native.NEO.GetCandidateVote(node))); + + var votes = contract.GetCandidateVote(node); + Assert.IsTrue(votes > 3_000_000, $"Votes: {votes}"); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs similarity index 60% rename from tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs rename to tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs index 8337c3fcb..6b085ecd4 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs @@ -1,19 +1,59 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Json; using Neo.Persistence; +using Neo.SmartContract.Testing.Storage; using System; +using System.IO; using System.Linq; using System.Text; -namespace Neo.SmartContract.Testing.UnitTests +namespace Neo.SmartContract.Testing.UnitTests.Storage { [TestClass] public class TestStorageTests { + [TestMethod] + public void TestCheckpoint() + { + // Create a new test engine with native contracts already initialized + + var engine = new TestEngine(true); + + // Check that all it works + + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + + // Create checkpoint + + var checkpoint = engine.Storage.Checkpoint(); + + // Create new storage, and restore the checkpoint on it + + var storage = new EngineStorage(new MemoryStore()); + checkpoint.Restore(storage.Snapshot); + + // Create new test engine without initialize + // and set the storage to the restored one + + engine = new TestEngine(false) { Storage = storage }; + + // Ensure that all works + + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + + // Test restoring in raw + + storage = new EngineStorage(new MemoryStore()); + new EngineCheckpoint(new MemoryStream(checkpoint.ToArray())).Restore(storage.Snapshot); + + engine = new TestEngine(false) { Storage = storage }; + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + } + [TestMethod] public void LoadExportImport() { - TestStorage store = new(new MemoryStore()); + EngineStorage store = new(new MemoryStore()); // empty @@ -51,7 +91,7 @@ public void LoadExportImport() // Test import - TestStorage storeCopy = new(new MemoryStore()); + EngineStorage storeCopy = new(new MemoryStore()); store.Commit(); storeCopy.Import(store.Export());