From fbc827f3f95524d9000ff7b77448786a5b2e5224 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Thu, 15 Feb 2024 13:53:55 +0100 Subject: [PATCH 01/23] Draft Checkpoint --- .../Storage/EngineCheckpoint.cs | 113 ++++++++++++++++++ .../EngineStorage.cs} | 12 +- src/Neo.SmartContract.Testing/TestEngine.cs | 3 +- .../{ => Storage}/TestStorageTests.cs | 7 +- 4 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 src/Neo.SmartContract.Testing/Storage/EngineCheckpoint.cs rename src/Neo.SmartContract.Testing/{TestStorage.cs => Storage/EngineStorage.cs} (93%) rename tests/Neo.SmartContract.Testing.UnitTests/{ => Storage}/TestStorageTests.cs (91%) 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 93% rename from src/Neo.SmartContract.Testing/TestStorage.cs rename to src/Neo.SmartContract.Testing/Storage/EngineStorage.cs index 5919c25e5..4a96721e9 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 } }; @@ -33,7 +33,7 @@ public class TestStorage /// Constructor /// /// Store - public TestStorage(IStore store) + public EngineStorage(IStore store) { Store = store; Snapshot = new SnapshotCache(Store.GetSnapshot()); @@ -56,6 +56,12 @@ public void Rollback() Snapshot = new SnapshotCache(Store.GetSnapshot()); } + /// + /// Get storage checkpoint + /// + /// StorageCheckpoint + public EngineCheckpoint Checkpoint() => new(Snapshot); + /// /// Import data from json, expected data (in base64): /// - "key" : "value" diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 57fada30f..594eaa3f3 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; init; } = new TestStorage(new MemoryStore()); + public EngineStorage Storage { get; init; } = new EngineStorage(new MemoryStore()); /// /// Protocol Settings diff --git a/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs similarity index 91% rename from tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs rename to tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs index 8337c3fcb..0c776a74a 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/TestStorageTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs @@ -1,11 +1,12 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Neo.Json; using Neo.Persistence; +using Neo.SmartContract.Testing.Storage; using System; using System.Linq; using System.Text; -namespace Neo.SmartContract.Testing.UnitTests +namespace Neo.SmartContract.TestEngine.UnitTests.Storage { [TestClass] public class TestStorageTests @@ -13,7 +14,7 @@ public class TestStorageTests [TestMethod] public void LoadExportImport() { - TestStorage store = new(new MemoryStore()); + EngineStorage store = new(new MemoryStore()); // empty @@ -51,7 +52,7 @@ public void LoadExportImport() // Test import - TestStorage storeCopy = new(new MemoryStore()); + EngineStorage storeCopy = new(new MemoryStore()); store.Commit(); storeCopy.Import(store.Export()); From dc39132e10996d2b255a8271f63ff2cf00942692 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Thu, 15 Feb 2024 13:55:50 +0100 Subject: [PATCH 02/23] fix namespaces --- .../Extensions/ArtifactExtensionsTests.cs | 2 +- .../Storage/TestStorageTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs index 66fa2633c..4c17508ad 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/TestStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs index 0c776a74a..641bbe563 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Text; -namespace Neo.SmartContract.TestEngine.UnitTests.Storage +namespace Neo.SmartContract.Testing.UnitTests.Storage { [TestClass] public class TestStorageTests From 5d41e75cb948cb1b36adbf8f7b4dfc71f4320518 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Thu, 15 Feb 2024 14:01:58 +0100 Subject: [PATCH 03/23] Add ut --- .../Storage/EngineStorage.cs | 2 +- .../Storage/TestStorageTests.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs index 4a96721e9..5242c9c05 100644 --- a/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs +++ b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs @@ -59,7 +59,7 @@ public void Rollback() /// /// Get storage checkpoint /// - /// StorageCheckpoint + /// EngineCheckpoint public EngineCheckpoint Checkpoint() => new(Snapshot); /// diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs index 641bbe563..487ab9cc3 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs @@ -3,6 +3,7 @@ using Neo.Persistence; using Neo.SmartContract.Testing.Storage; using System; +using System.IO; using System.Linq; using System.Text; @@ -11,6 +12,32 @@ namespace Neo.SmartContract.Testing.UnitTests.Storage [TestClass] public class TestStorageTests { + [TestMethod] + public void TestCheckpoint() + { + var engine = new TestEngine(true); + + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + + var checkpoint = engine.Storage.Checkpoint(); + + // Test restoring the checkpoint + + var storage = new EngineStorage(new MemoryStore()); + checkpoint.Restore(storage.Snapshot); + + engine = new TestEngine(false) { Storage = storage }; + 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() { From 469502fcb9a0ce9040caf36f1c2595e8ff0da0fc Mon Sep 17 00:00:00 2001 From: Shargon Date: Thu, 15 Feb 2024 12:06:41 -0800 Subject: [PATCH 04/23] Update src/Neo.SmartContract.Testing/Storage/EngineStorage.cs Co-authored-by: Jimmy --- src/Neo.SmartContract.Testing/Storage/EngineStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs index 5242c9c05..1eed26352 100644 --- a/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs +++ b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs @@ -32,7 +32,7 @@ public class EngineStorage /// /// Constructor /// - /// Store + /// Store public EngineStorage(IStore store) { Store = store; From fef4902cdaa588f68596db6bd86a5b0891b218bd Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 00:39:04 +0100 Subject: [PATCH 05/23] Rpc Storage --- .../Storage/Rpc/RpcSnapshot.cs | 64 +++++++++++++ .../Storage/Rpc/RpcStore.cs | 95 +++++++++++++++++++ src/Neo.SmartContract.Testing/TestEngine.cs | 4 +- .../Storage/Rpc/RpcStoreTests.cs | 43 +++++++++ 4 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 src/Neo.SmartContract.Testing/Storage/Rpc/RpcSnapshot.cs create mode 100644 src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs 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..a0f087e0c --- /dev/null +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -0,0 +1,95 @@ +using Neo.Persistence; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +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 void Dispose() { } + public ISnapshot GetSnapshot() => new RpcSnapshot(this); + public bool Contains(byte[] key) => TryGet(key) != null; + + #region Rpc calls + + public IEnumerable<(byte[] Key, byte[] Value)> Seek(byte[] key, SeekDirection direction) + { + // TODO: This could be supported in a future + + throw new NotImplementedException(); + } + + 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(); + + if (!response.IsSuccessStatusCode) + { + throw new NotImplementedException(); + } + + 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 NotImplementedException(); + } + } + + #endregion +} diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 594eaa3f3..00a790c58 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -186,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(), 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..1f3f34cbe --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs @@ -0,0 +1,43 @@ +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://seed2.neo.org:10332")) + }; + + // check network values + + Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); + Assert.IsTrue(engine.Native.Ledger.CurrentIndex > 4_905_187); + + // check deploy + + var node = ECPoint.Parse("03d9e8b16bd9b22d3345d6d4cde31be1c3e1d161532e3d0ccecb95ece2eb58336e", ECCurve.Secp256k1); + 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 > 5_000_000, $"Votes: {votes}"); + } + } +} From 159cbbd244b88d3d88fb46d1673aafaac70aac4f Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 01:07:16 +0100 Subject: [PATCH 06/23] Seek --- .../Storage/Rpc/RpcStore.cs | 71 ++++++++++++++++++- .../Storage/Rpc/RpcStoreTests.cs | 4 ++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs index a0f087e0c..b82bede1f 100644 --- a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -35,15 +35,82 @@ 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 void Dispose() { } 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) { - // TODO: This could be supported in a future + if (direction is SeekDirection.Backward) + { + // TODO: not implemented in RPC + + throw new NotImplementedException(); + } + + 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(); + + if (!response.IsSuccessStatusCode) + { + throw new NotImplementedException(); + } + + JObject jo = JObject.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); + + if (jo["result"]?["results"]?.Value() is JArray results) + { + // iterate page + + foreach (JObject result in results) + { + if (result["key"]?.Value() is string jkey && + result["value"]?.Value() is string kvalue) + { + yield return (Convert.FromBase64String(jkey), Convert.FromBase64String(kvalue)); + } + } + + if (jo["truncated"]?.Value() == true && + jo["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 NotImplementedException(); + } + } throw new NotImplementedException(); } diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs index 1f3f34cbe..0d9a0f1af 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs @@ -29,6 +29,10 @@ public void TestRpcStore() Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); Assert.IsTrue(engine.Native.Ledger.CurrentIndex > 4_905_187); + // check with Seek (RPC doesn't support Backward) + + // Assert.IsTrue(engine.Native.NEO.GasPerBlock == 5); + // check deploy var node = ECPoint.Parse("03d9e8b16bd9b22d3345d6d4cde31be1c3e1d161532e3d0ccecb95ece2eb58336e", ECCurve.Secp256k1); From d1f65db79aeadbe45eb211ad33511ee0d5b78a85 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 01:11:18 +0100 Subject: [PATCH 07/23] clean --- .../Storage/Rpc/RpcStore.cs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs index b82bede1f..d1859232b 100644 --- a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -65,12 +65,7 @@ public void Dispose() { } using var httpClient = new HttpClient(); var requestContent = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json"); - var response = httpClient.PostAsync(Url, requestContent).GetAwaiter().GetResult(); - - if (!response.IsSuccessStatusCode) - { - throw new NotImplementedException(); - } + var response = httpClient.PostAsync(Url, requestContent).GetAwaiter().GetResult().EnsureSuccessStatusCode(); JObject jo = JObject.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); @@ -108,11 +103,11 @@ public void Dispose() { } yield break; } - throw new NotImplementedException(); + throw new Exception(); } } - throw new NotImplementedException(); + throw new Exception(); } public byte[]? TryGet(byte[] key) @@ -128,12 +123,7 @@ public void Dispose() { } using var httpClient = new HttpClient(); var requestContent = new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json"); - var response = httpClient.PostAsync(Url, requestContent).GetAwaiter().GetResult(); - - if (!response.IsSuccessStatusCode) - { - throw new NotImplementedException(); - } + var response = httpClient.PostAsync(Url, requestContent).GetAwaiter().GetResult().EnsureSuccessStatusCode(); JObject jo = JObject.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); @@ -154,7 +144,7 @@ public void Dispose() { } return null; } - throw new NotImplementedException(); + throw new Exception(); } } From fa60aae1d9b4cf12c2a5b6e1be5fee4a77a25786 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 02:03:36 +0100 Subject: [PATCH 08/23] By pass RPC Backward --- .../Storage/Rpc/RpcStore.cs | 36 ++++++++++++++----- .../Storage/Rpc/RpcStoreTests.cs | 4 +-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs index d1859232b..e9999e768 100644 --- a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -1,8 +1,12 @@ +using Neo.IO; using Neo.Persistence; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading; @@ -45,9 +49,25 @@ public void Dispose() { } { if (direction is SeekDirection.Backward) { - // TODO: not implemented in RPC + // 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 - throw new NotImplementedException(); + 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(prefix.Concat(entry.Key).ToArray(), 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); @@ -69,21 +89,21 @@ public void Dispose() { } JObject jo = JObject.Parse(response.Content.ReadAsStringAsync().GetAwaiter().GetResult()); - if (jo["result"]?["results"]?.Value() is JArray results) + if (jo["result"]?.Value() is JObject result && result["results"]?.Value() is JArray results) { // iterate page - foreach (JObject result in results) + foreach (JObject r in results) { - if (result["key"]?.Value() is string jkey && - result["value"]?.Value() is string kvalue) + if (r["key"]?.Value() is string jkey && + r["value"]?.Value() is string kvalue) { yield return (Convert.FromBase64String(jkey), Convert.FromBase64String(kvalue)); } } - if (jo["truncated"]?.Value() == true && - jo["next"]?.Value() is int next) + if (result["truncated"]?.Value() == true && + result["next"]?.Value() is int next) { start = next; } diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs index 0d9a0f1af..aa97ca78d 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs @@ -29,9 +29,9 @@ public void TestRpcStore() Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); Assert.IsTrue(engine.Native.Ledger.CurrentIndex > 4_905_187); - // check with Seek (RPC doesn't support Backward) + // check with Seek (RPC doesn't support Backward, it could be slow) - // Assert.IsTrue(engine.Native.NEO.GasPerBlock == 5); + Assert.IsTrue(engine.Native.NEO.GasPerBlock == 500000000); // check deploy From 71592fc3df2c8708120ba684292fcfdccfa38dfe Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 02:12:39 +0100 Subject: [PATCH 09/23] Change to testnet --- src/Neo.SmartContract.Testing/Storage/EngineStorage.cs | 9 +++++++++ .../Storage/Rpc/RpcStoreTests.cs | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs index 1eed26352..847eae0bb 100644 --- a/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs +++ b/src/Neo.SmartContract.Testing/Storage/EngineStorage.cs @@ -62,6 +62,15 @@ public void Rollback() /// 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/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs index aa97ca78d..a18535ed9 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/Rpc/RpcStoreTests.cs @@ -21,13 +21,13 @@ public void TestRpcStore() { var engine = new TestEngine(false) { - Storage = new EngineStorage(new RpcStore("http://seed2.neo.org:10332")) + 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 > 4_905_187); + Assert.IsTrue(engine.Native.Ledger.CurrentIndex > 3_510_270); // check with Seek (RPC doesn't support Backward, it could be slow) @@ -35,13 +35,13 @@ public void TestRpcStore() // check deploy - var node = ECPoint.Parse("03d9e8b16bd9b22d3345d6d4cde31be1c3e1d161532e3d0ccecb95ece2eb58336e", ECCurve.Secp256k1); + 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 > 5_000_000, $"Votes: {votes}"); + Assert.IsTrue(votes > 3_000_000, $"Votes: {votes}"); } } } From a93037e699c606305e9350c3ca3cadddbccc1efa Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 02:22:46 +0100 Subject: [PATCH 10/23] Fix bug --- src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs index e9999e768..8825f4f6e 100644 --- a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -59,7 +59,7 @@ public void Dispose() { } foreach (var entry in Seek(key.Take(key.Length == 4 ? 4 : 5).ToArray(), SeekDirection.Forward)) { - data.TryAdd(prefix.Concat(entry.Key).ToArray(), entry.Value); + data.TryAdd(entry.Key, entry.Value); } foreach (var entry in new MemorySnapshot(data).Seek(key, direction)) @@ -93,12 +93,14 @@ public void Dispose() { } { // 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 (Convert.FromBase64String(jkey), Convert.FromBase64String(kvalue)); + yield return (prefix.Concat(Convert.FromBase64String(jkey)).ToArray(), Convert.FromBase64String(kvalue)); } } From 2640e16817b8613ac18895043690c7c3ad294c60 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Fri, 16 Feb 2024 02:23:11 +0100 Subject: [PATCH 11/23] clean using --- src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs index 8825f4f6e..3c0bf8446 100644 --- a/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs +++ b/src/Neo.SmartContract.Testing/Storage/Rpc/RpcStore.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; -using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; From c89f9f828a574aa688376b0d1e43e5d1c51ab6ec Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 08:37:04 +0100 Subject: [PATCH 12/23] Conflicts --- src/Neo.SmartContract.Testing/TestEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 48e62cb3c..637b3817a 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -72,7 +72,7 @@ public class TestEngine /// /// Storage /// - public EngineStorage Storage { get; init; } = new EngineStorage(new MemoryStore()); + public EngineStorage Storage { get; set; } = new EngineStorage(new MemoryStore()); /// /// Protocol Settings From cc0f6cb89b2a162b98ac016f20967cfdda76a5d1 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 09:15:12 +0100 Subject: [PATCH 13/23] Dump to html --- .../Coverage/CoverageHit.cs | 31 +++- .../Coverage/CoveredContract.cs | 155 +++++++++++++++--- .../Coverage/CoveredMethod.cs | 6 + .../Coverage/DumpFormat.cs | 15 ++ .../TestingApplicationEngine.cs | 2 +- .../neocontractnep17/CoverageContractTests.cs | 2 + .../Coverage/CoverageDataTests.cs | 12 +- 7 files changed, 194 insertions(+), 29 deletions(-) create mode 100644 src/Neo.SmartContract.Testing/Coverage/DumpFormat.cs diff --git a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs index d419da70c..b72bb0e2a 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs @@ -1,9 +1,10 @@ +using Neo.VM; 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}")] + [DebuggerDisplay("Offset:{Offset}, Description:{Description}, OutOfScript:{OutOfScript}, Hits:{Hits}, GasTotal:{GasTotal}, GasMin:{GasMin}, GasMax:{GasMax}, GasAvg:{GasAvg}")] public class CoverageHit { /// @@ -11,6 +12,11 @@ public class CoverageHit /// public int Offset { get; } + /// + /// The instruction description + /// + public string Description { get; } + /// /// The instruction is out of the script /// @@ -45,10 +51,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 +112,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 +121,28 @@ public CoverageHit Clone() }; } + /// + /// Return description from instruction + /// + /// Instruction + /// Description + public static string DescriptionFromInstruction(Instruction instruction) + { + if (instruction.Operand.Length > 0) + { + return instruction.OpCode.ToString() + " " + instruction.Operand.ToArray().ToHexString(false); + } + + 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..a39ac08f5 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; } } @@ -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..3311c5e7c 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs @@ -46,6 +46,12 @@ public CoveredMethod(CoveredContract contract, ContractMethodDescriptor method, MethodLength = methodLength; } + /// + /// 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/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 3ce77e5d5..63905616a 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -65,7 +65,7 @@ protected override void PostExecuteInstruction(Instruction instruction) if (InstructionPointer is null) return; - coveredContract.Hit(InstructionPointer.Value, GasConsumed - PreExecuteInstructionGasConsumed); + coveredContract.Hit(InstructionPointer.Value, instruction, GasConsumed - PreExecuteInstructionGasConsumed); } protected override void OnSysCall(InteropDescriptor descriptor) diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs index f242a2a8b..99b4f97db 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs @@ -21,6 +21,8 @@ public static void EnsureCoverage() 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}"); } } diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs index 4f6296337..36a544029 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs @@ -43,6 +43,15 @@ 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5 [5.26%] │ vote,2 │ 0.00% │ └-────────────────────────-┴-───────-┘ ".Trim(), engine.GetCoverage(engine.Native.NEO)?.Dump().Trim()); + + Assert.AreEqual(@" +0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5 [5.26%] +┌-─────────────-┬-───────-┐ +│ Method │ Line │ +├-─────────────-┼-───────-┤ +│ totalSupply,0 │ 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); From f8046be5a8a444f55deaf339ec4ba5124a64337e Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 09:27:35 +0100 Subject: [PATCH 14/23] Fix coverage during OnFault --- .../TestingApplicationEngine.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 63905616a..52b3ac538 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -11,6 +11,7 @@ namespace Neo.SmartContract.Testing ///
internal class TestingApplicationEngine : ApplicationEngine { + private Instruction? PreInstruction; private ExecutionContext? InstructionContext; private int? InstructionPointer; private long PreExecuteInstructionGasConsumed; @@ -32,6 +33,7 @@ protected override void PreExecuteInstruction(Instruction instruction) if (Engine.EnableCoverageCapture) { + PreInstruction = instruction; PreExecuteInstructionGasConsumed = GasConsumed; InstructionContext = CurrentContext; InstructionPointer = InstructionContext?.InstructionPointer; @@ -42,10 +44,24 @@ protected override void PreExecuteInstruction(Instruction instruction) base.PreExecuteInstruction(instruction); } + protected override void OnFault(Exception ex) + { + base.OnFault(ex); + + if (PreInstruction is not null) + { + 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; @@ -66,6 +82,10 @@ protected override void PostExecuteInstruction(Instruction instruction) if (InstructionPointer is null) return; coveredContract.Hit(InstructionPointer.Value, instruction, GasConsumed - PreExecuteInstructionGasConsumed); + + PreInstruction = null; + InstructionContext = null; + InstructionPointer = null; } protected override void OnSysCall(InteropDescriptor descriptor) From a265017d1503fdd46bd7e2d55c8104fd48e8b51a Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 09:42:27 +0100 Subject: [PATCH 15/23] print string when possible --- .../Coverage/CoverageHit.cs | 8 +++++++- .../neocontractnep17/OwnerContractTests.cs | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs index b72bb0e2a..38570503d 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs @@ -1,6 +1,7 @@ using Neo.VM; using System; using System.Diagnostics; +using System.Text.RegularExpressions; namespace Neo.SmartContract.Testing.Coverage { @@ -130,7 +131,12 @@ public static string DescriptionFromInstruction(Instruction instruction) { if (instruction.Operand.Length > 0) { - return instruction.OpCode.ToString() + " " + instruction.Operand.ToArray().ToHexString(false); + if (instruction.Operand.Span.TryGetString(out var str) && Regex.IsMatch(str, @"^[a-zA-Z0-9_]+$")) + { + return instruction.OpCode.ToString() + $" '{str}'"; + } + + return instruction.OpCode.ToString() + " " + instruction.Operand.ToArray().ToHexString(); } return instruction.OpCode.ToString(); 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); + } } } From 9ce83534f67eec7f447015859462b209236ea02d Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 11:51:12 +0100 Subject: [PATCH 16/23] Coverage 100% --- .../Coverage/CoverageHit.cs | 37 +++++++++- .../Extensions/TestExtensions.cs | 1 + .../SmartContract.cs | 4 ++ src/Neo.SmartContract.Testing/TestEngine.cs | 11 +++ .../TestingApplicationEngine.cs | 3 + .../TestingStandards/Nep17Tests.cs | 70 +++++++++++++++++++ .../neocontractnep17/CoverageContractTests.cs | 4 +- .../neocontractnep17/Nep17ContractTests.cs | 6 ++ 8 files changed, 132 insertions(+), 4 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs index 38570503d..872299c45 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs @@ -131,12 +131,45 @@ 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 instruction.OpCode.ToString() + $" '{str}'"; + return ret + $" '{str}'"; } - return instruction.OpCode.ToString() + " " + instruction.Operand.ToArray().ToHexString(); + return ret; } return instruction.OpCode.ToString(); 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/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/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 637b3817a..de753e41d 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -445,6 +445,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; @@ -595,6 +601,11 @@ 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 + // It 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 52b3ac538..335196d30 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 @@ -138,6 +139,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..264f8b7ba 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(); + + // Return to Alice, mock is the caller + + 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 99b4f97db..622222bd1 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs @@ -8,7 +8,7 @@ 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() @@ -23,7 +23,7 @@ public static void EnsureCoverage() 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}"); + 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; From ac0100984557bc1b7b8b6238624662dd26a7dfdc Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 11:53:53 +0100 Subject: [PATCH 17/23] fix comment --- src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs index 264f8b7ba..a2981bf70 100644 --- a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs +++ b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs @@ -199,7 +199,7 @@ public virtual void TestTransfer() calledAmount = (BigInteger)i.Arguments[1]; calledData = (i.Arguments[2] as ByteString)!.GetSpan().ToArray(); - // Return to Alice, mock is the caller + // Ensure the event was called var me = new UInt160(calledData); AssertTransferEvent(Alice.Account, me, calledAmount); From 30f45bd4e51b8a2e4f499dd69c92d1cff741b161 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 12:31:54 +0100 Subject: [PATCH 18/23] Update README --- src/Neo.Compiler.CSharp/Options.cs | 2 +- src/Neo.Compiler.CSharp/Program.cs | 4 +- src/Neo.SmartContract.Testing/README.md | 97 +++++++++++++++++-- src/Neo.SmartContract.Testing/TestEngine.cs | 33 ++++++- .../neocontractnep17/CoverageContractTests.cs | 5 +- .../Storage/TestStorageTests.cs | 14 ++- 6 files changed, 136 insertions(+), 19 deletions(-) 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/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/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index de753e41d..7a0843bee 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -234,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 /// @@ -242,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); } /// @@ -601,8 +627,7 @@ 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 - // It will work + // Ensure that if we convert to BigInteger this value will work if (data[0] == 0) data[0] = 1; diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs index 622222bd1..ddfc675f7 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs @@ -13,13 +13,14 @@ public class CoverageContractTests [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); + // 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)); diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs index 487ab9cc3..6b085ecd4 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Storage/TestStorageTests.cs @@ -15,18 +15,30 @@ 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(); - // Test restoring the 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 From f0ec6983a646ff9318795a0a7ce800fad0b89156 Mon Sep 17 00:00:00 2001 From: Shargon Date: Sat, 17 Feb 2024 03:56:57 -0800 Subject: [PATCH 19/23] Update src/Neo.SmartContract.Testing/TestingApplicationEngine.cs --- src/Neo.SmartContract.Testing/TestingApplicationEngine.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 335196d30..874877df6 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -51,6 +51,7 @@ protected override void OnFault(Exception ex) if (PreInstruction is not null) { + // PostExecuteInstruction is not executed onFault RecoverCoverage(PreInstruction); } } From 26ad4a1be1a7392edec463552269f9b067932613 Mon Sep 17 00:00:00 2001 From: Shargon Date: Sat, 17 Feb 2024 03:57:20 -0800 Subject: [PATCH 20/23] format --- src/Neo.SmartContract.Testing/TestingApplicationEngine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 874877df6..71ac969d5 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -51,7 +51,7 @@ protected override void OnFault(Exception ex) if (PreInstruction is not null) { - // PostExecuteInstruction is not executed onFault + // PostExecuteInstruction is not executed onFault RecoverCoverage(PreInstruction); } } From 98e33867d6d16fb91d5d98d48209840ca9782fa1 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 13:15:18 +0100 Subject: [PATCH 21/23] Improve method name --- .../Coverage/AbiMethod.cs | 7 ++- .../Coverage/CoveredMethod.cs | 3 +- .../Coverage/CoverageDataTests.cs | 48 +++++++++---------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs b/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs index c0e743c44..dfdf8bb9f 100644 --- a/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs +++ b/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs @@ -9,6 +9,8 @@ namespace Neo.SmartContract.Testing.Coverage [DebuggerDisplay("{Name},{PCount}")] public class AbiMethod : IEquatable { + private readonly string _toString; + /// /// Method name /// @@ -24,10 +26,11 @@ public class AbiMethod : IEquatable /// /// Method name /// Parameters count - public AbiMethod(string name, int pCount) + public AbiMethod(string name, int pCount, string? toString = null) { Name = name; PCount = pCount; + _toString = toString ?? $"{Name},{PCount}"; } /// @@ -89,6 +92,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/CoveredMethod.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs index 3311c5e7c..ab56fc2aa 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,9 +42,9 @@ 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.Length, method.Name + $"({string.Join(",", method.Parameters.Select(u => u.Name))})"); } /// diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs index 36a544029..3af364092 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs @@ -19,29 +19,29 @@ 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(@" @@ -49,7 +49,7 @@ 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5 [5.26%] ┌-─────────────-┬-───────-┐ │ Method │ Line │ ├-─────────────-┼-───────-┤ -│ totalSupply,0 │ 100.00% │ +│ totalSupply() │ 100.00% │ └-─────────────-┴-───────-┘ ".Trim(), (engine.Native.NEO.GetCoverage(o => o.TotalSupply) as CoveredMethod)?.Dump().Trim()); } From 4b492d6f2be290cf705bf5ca04720de01d771851 Mon Sep 17 00:00:00 2001 From: Fernando Diaz Toledano Date: Sat, 17 Feb 2024 13:22:31 +0100 Subject: [PATCH 22/23] Refactor AbiMethod constructor --- .../Coverage/AbiMethod.cs | 17 +++++++++-------- .../Coverage/CoveredContract.cs | 2 +- .../Coverage/CoveredMethod.cs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs b/src/Neo.SmartContract.Testing/Coverage/AbiMethod.cs index dfdf8bb9f..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; @@ -25,12 +26,12 @@ public class AbiMethod : IEquatable /// Constructor /// /// Method name - /// Parameters count - public AbiMethod(string name, int pCount, string? toString = null) + /// Arguments names + public AbiMethod(string name, string[] argsName) { Name = name; - PCount = pCount; - _toString = toString ?? $"{Name},{PCount}"; + PCount = argsName.Length; + _toString = name + $"({string.Join(",", argsName)})"; } /// @@ -58,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()) }; } } } @@ -76,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()) }; } } diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs index a39ac08f5..a64ffbb62 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs @@ -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); } /// diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs index ab56fc2aa..0ed2aa80f 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs @@ -44,7 +44,7 @@ public CoveredMethod(CoveredContract contract, ContractMethodDescriptor method, Contract = contract; Offset = method.Offset; MethodLength = methodLength; - Method = new AbiMethod(method.Name, method.Parameters.Length, method.Name + $"({string.Join(",", method.Parameters.Select(u => u.Name))})"); + Method = new AbiMethod(method.Name, method.Parameters.Select(u => u.Name).ToArray()); } /// From 4b4351499e12b33ad5f24e469174574e399b9edb Mon Sep 17 00:00:00 2001 From: Shargon Date: Sun, 18 Feb 2024 12:25:24 -0800 Subject: [PATCH 23/23] Update tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs --- .../templates/neocontractnep17/CoverageContractTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs index ddfc675f7..606af9e30 100644 --- a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs @@ -18,7 +18,7 @@ public static void EnsureCoverage() var coverage = Nep17ContractTests.Coverage; coverage?.Join(OwnerContractTests.Coverage); - // Ennsure that the coverage is more than X% at the end of the tests + // Ensure that the coverage is more than X% at the end of the tests Assert.IsNotNull(coverage); Console.WriteLine(coverage.Dump());