From 120d87683f3e851c0f3562485b3e7aac7b66b005 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Fri, 16 Feb 2024 04:06:25 +0800 Subject: [PATCH 01/22] [Compiler] Further Split long expression files (#884) * add comments to expression * change namespace style * remove * update neo to latest * adapt to latest neo version * dotnet format * Apply suggestions from code review Clean empty lines * remove comments * update neo * Update BinaryExpression.cs * Update src/Neo.Compiler.CSharp/MethodConvert/Expression/CastExpression.cs * Update src/Neo.Compiler.CSharp/MethodConvert/Expression/CastExpression.cs * merge master --------- Co-authored-by: Shargon --- neo | 2 +- neo-devpack-dotnet.sln | 14 + ...AssignmentExpression.CoalesceAssignment.cs | 300 ++++++++ .../AssignmentExpression.ComplexAssignment.cs | 256 +++++++ .../AssignmentExpression.SimpleAssignment.cs | 197 +++++ .../Expression/AssignmentExpression.cs | 686 ------------------ .../UnaryExpression.PostfixUnary.cs | 235 ++++++ ...sion.cs => UnaryExpression.PrefixUnary.cs} | 212 ------ .../MethodConvert/MethodConvert.cs | 3 - 9 files changed, 1003 insertions(+), 902 deletions(-) create mode 100644 src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs create mode 100644 src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs create mode 100644 src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs create mode 100644 src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs rename src/Neo.Compiler.CSharp/MethodConvert/Expression/{UnaryExpression.cs => UnaryExpression.PrefixUnary.cs} (54%) diff --git a/neo b/neo index 4a5a7f585..fd3d68d52 160000 --- a/neo +++ b/neo @@ -1 +1 @@ -Subproject commit 4a5a7f58567c1bf270d1c5515d90486c803b4e11 +Subproject commit fd3d68d527322d150cd9d850440418ac7644c8b3 diff --git a/neo-devpack-dotnet.sln b/neo-devpack-dotnet.sln index 2a5c1b4cd..02f55daa4 100644 --- a/neo-devpack-dotnet.sln +++ b/neo-devpack-dotnet.sln @@ -36,6 +36,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Testing", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Testing.UnitTests", "tests\Neo.SmartContract.Testing.UnitTests\Neo.SmartContract.Testing.UnitTests.csproj", "{B772B8A9-9362-4C6F-A6D3-2A4138439B2C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions", "neo\src\Neo.Extensions\Neo.Extensions.csproj", "{E5EFB018-810D-4297-8921-940FA0B1ED97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.IO", "neo\src\Neo.IO\Neo.IO.csproj", "{C2B7927F-AAA5-432A-8E76-B5080BD7EFB9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,6 +102,14 @@ Global {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Release|Any CPU.Build.0 = Release|Any CPU + {E5EFB018-810D-4297-8921-940FA0B1ED97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5EFB018-810D-4297-8921-940FA0B1ED97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5EFB018-810D-4297-8921-940FA0B1ED97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5EFB018-810D-4297-8921-940FA0B1ED97}.Release|Any CPU.Build.0 = Release|Any CPU + {C2B7927F-AAA5-432A-8E76-B5080BD7EFB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C2B7927F-AAA5-432A-8E76-B5080BD7EFB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C2B7927F-AAA5-432A-8E76-B5080BD7EFB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C2B7927F-AAA5-432A-8E76-B5080BD7EFB9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -117,6 +129,8 @@ Global {D6D53889-5A10-46A4-BA66-E78B56EC1881} = {49D5873D-7B38-48A5-B853-85146F032091} {648DCE6F-A0BA-4032-951B-20CF5BBFD998} = {79389FC0-C621-4CEA-AD2B-6074C32E7BCA} {B772B8A9-9362-4C6F-A6D3-2A4138439B2C} = {D5266066-0AFD-44D5-A83E-2F73668A63C8} + {E5EFB018-810D-4297-8921-940FA0B1ED97} = {49D5873D-7B38-48A5-B853-85146F032091} + {C2B7927F-AAA5-432A-8E76-B5080BD7EFB9} = {49D5873D-7B38-48A5-B853-85146F032091} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DA935E1-C674-4364-B087-F1B511B79215} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs new file mode 100644 index 000000000..5ff26de6c --- /dev/null +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.CoalesceAssignment.cs @@ -0,0 +1,300 @@ +// Copyright (C) 2015-2023 The Neo Project. +// +// The Neo.Compiler.CSharp is free software distributed under the MIT +// software license, see the accompanying file LICENSE in the main directory +// of the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +extern alias scfx; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.VM; +using System; +using System.Runtime.InteropServices; + +namespace Neo.Compiler; + +partial class MethodConvert +{ + private void ConvertCoalesceAssignmentExpression(SemanticModel model, AssignmentExpressionSyntax expression) + { + switch (expression.Left) + { + case ElementAccessExpressionSyntax left: + ConvertElementAccessCoalesceAssignment(model, left, expression.Right); + break; + case IdentifierNameSyntax left: + ConvertIdentifierNameCoalesceAssignment(model, left, expression.Right); + break; + case MemberAccessExpressionSyntax left: + ConvertMemberAccessCoalesceAssignment(model, left, expression.Right); + break; + default: + throw new CompilationException(expression, DiagnosticId.SyntaxNotSupported, $"Unsupported coalesce assignment: {expression}"); + } + } + + private void ConvertElementAccessCoalesceAssignment(SemanticModel model, ElementAccessExpressionSyntax left, ExpressionSyntax right) + { + if (left.ArgumentList.Arguments.Count != 1) + throw new CompilationException(left.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {left.ArgumentList.Arguments}"); + JumpTarget assignmentTarget = new(); + JumpTarget endTarget = new(); + if (model.GetSymbolInfo(left).Symbol is IPropertySymbol property) + { + ConvertExpression(model, left.Expression); + ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + Call(model, property.GetMethod!, CallingConvention.StdCall); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AddInstruction(OpCode.NIP); + AddInstruction(OpCode.NIP); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.DROP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + Call(model, property.SetMethod!, CallingConvention.Cdecl); + } + else + { + ConvertExpression(model, left.Expression); + ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.PICKITEM); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AddInstruction(OpCode.PICKITEM); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + AddInstruction(OpCode.REVERSE3); + AddInstruction(OpCode.SETITEM); + } + endTarget.Instruction = AddInstruction(OpCode.NOP); + } + + private void ConvertIdentifierNameCoalesceAssignment(SemanticModel model, IdentifierNameSyntax left, ExpressionSyntax right) + { + ISymbol symbol = model.GetSymbolInfo(left).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + ConvertFieldIdentifierNameCoalesceAssignment(model, field, right); + break; + case ILocalSymbol local: + ConvertLocalIdentifierNameCoalesceAssignment(model, local, right); + break; + case IParameterSymbol parameter: + ConvertParameterIdentifierNameCoalesceAssignment(model, parameter, right); + break; + case IPropertySymbol property: + ConvertPropertyIdentifierNameCoalesceAssignment(model, property, right); + break; + default: + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertMemberAccessCoalesceAssignment(SemanticModel model, MemberAccessExpressionSyntax left, ExpressionSyntax right) + { + ISymbol symbol = model.GetSymbolInfo(left).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + ConvertFieldMemberAccessCoalesceAssignment(model, left, right, field); + break; + case IPropertySymbol property: + ConvertPropertyMemberAccessCoalesceAssignment(model, left, right, property); + break; + default: + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertFieldIdentifierNameCoalesceAssignment(SemanticModel model, IFieldSymbol left, ExpressionSyntax right) + { + JumpTarget assignmentTarget = new(); + JumpTarget endTarget = new(); + if (left.IsStatic) + { + byte index = context.AddStaticField(left); + AccessSlot(OpCode.LDSFLD, index); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AccessSlot(OpCode.LDSFLD, index); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(left.ContainingType.GetFields(), left); + AddInstruction(OpCode.LDARG0); + Push(index); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.PICKITEM); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AddInstruction(OpCode.PICKITEM); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + AddInstruction(OpCode.REVERSE3); + AddInstruction(OpCode.SETITEM); + } + endTarget.Instruction = AddInstruction(OpCode.NOP); + } + + private void ConvertLocalIdentifierNameCoalesceAssignment(SemanticModel model, ILocalSymbol left, ExpressionSyntax right) + { + JumpTarget assignmentTarget = new(); + JumpTarget endTarget = new(); + byte index = _localVariables[left]; + AccessSlot(OpCode.LDLOC, index); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AccessSlot(OpCode.LDLOC, index); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STLOC, index); + endTarget.Instruction = AddInstruction(OpCode.NOP); + } + + private void ConvertParameterIdentifierNameCoalesceAssignment(SemanticModel model, IParameterSymbol left, ExpressionSyntax right) + { + JumpTarget assignmentTarget = new(); + JumpTarget endTarget = new(); + byte index = _parameters[left]; + AccessSlot(OpCode.LDARG, index); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AccessSlot(OpCode.LDARG, index); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STARG, index); + endTarget.Instruction = AddInstruction(OpCode.NOP); + } + + private void ConvertPropertyIdentifierNameCoalesceAssignment(SemanticModel model, IPropertySymbol left, ExpressionSyntax right) + { + JumpTarget endTarget = new(); + if (left.IsStatic) + { + Call(model, left.GetMethod!); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIFNOT_L, endTarget); + AddInstruction(OpCode.DROP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + Call(model, left.SetMethod!); + } + else + { + AddInstruction(OpCode.LDARG0); + Call(model, left.GetMethod!); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIFNOT_L, endTarget); + AddInstruction(OpCode.DROP); + AddInstruction(OpCode.LDARG0); + ConvertExpression(model, right); + AddInstruction(OpCode.TUCK); + Call(model, left.SetMethod!, CallingConvention.StdCall); + } + endTarget.Instruction = AddInstruction(OpCode.NOP); + } + + private void ConvertFieldMemberAccessCoalesceAssignment(SemanticModel model, MemberAccessExpressionSyntax left, ExpressionSyntax right, IFieldSymbol field) + { + JumpTarget assignmentTarget = new(); + JumpTarget endTarget = new(); + if (field.IsStatic) + { + byte index = context.AddStaticField(field); + AccessSlot(OpCode.LDSFLD, index); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AccessSlot(OpCode.LDSFLD, index); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(field.ContainingType.GetFields(), field); + ConvertExpression(model, left.Expression); + Push(index); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.PICKITEM); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AddInstruction(OpCode.PICKITEM); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.NOP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + AddInstruction(OpCode.REVERSE3); + AddInstruction(OpCode.SETITEM); + } + endTarget.Instruction = AddInstruction(OpCode.NOP); + } + + private void ConvertPropertyMemberAccessCoalesceAssignment(SemanticModel model, MemberAccessExpressionSyntax left, ExpressionSyntax right, IPropertySymbol property) + { + JumpTarget endTarget = new(); + if (property.IsStatic) + { + Call(model, property.GetMethod!); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIFNOT_L, endTarget); + AddInstruction(OpCode.DROP); + ConvertExpression(model, right); + AddInstruction(OpCode.DUP); + Call(model, property.SetMethod!); + } + else + { + JumpTarget assignmentTarget = new(); + ConvertExpression(model, left.Expression); + AddInstruction(OpCode.DUP); + Call(model, property.GetMethod!); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.ISNULL); + Jump(OpCode.JMPIF_L, assignmentTarget); + AddInstruction(OpCode.NIP); + Jump(OpCode.JMP_L, endTarget); + assignmentTarget.Instruction = AddInstruction(OpCode.DROP); + ConvertExpression(model, right); + AddInstruction(OpCode.TUCK); + Call(model, property.SetMethod!, CallingConvention.StdCall); + } + endTarget.Instruction = AddInstruction(OpCode.NOP); + } +} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs new file mode 100644 index 000000000..626d906d2 --- /dev/null +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.ComplexAssignment.cs @@ -0,0 +1,256 @@ +// Copyright (C) 2015-2023 The Neo Project. +// +// The Neo.Compiler.CSharp is free software distributed under the MIT +// software license, see the accompanying file LICENSE in the main directory +// of the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +extern alias scfx; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.VM; +using System; +using System.Runtime.InteropServices; + +namespace Neo.Compiler; + +partial class MethodConvert +{ + private void ConvertComplexAssignmentExpression(SemanticModel model, AssignmentExpressionSyntax expression) + { + ITypeSymbol type = model.GetTypeInfo(expression).Type!; + switch (expression.Left) + { + case ElementAccessExpressionSyntax left: + ConvertElementAccessComplexAssignment(model, type, expression.OperatorToken, left, expression.Right); + break; + case IdentifierNameSyntax left: + ConvertIdentifierNameComplexAssignment(model, type, expression.OperatorToken, left, expression.Right); + break; + case MemberAccessExpressionSyntax left: + ConvertMemberAccessComplexAssignment(model, type, expression.OperatorToken, left, expression.Right); + break; + default: + throw new CompilationException(expression.Left, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment expression: {expression}"); + } + } + + private void ConvertElementAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, ElementAccessExpressionSyntax left, ExpressionSyntax right) + { + if (left.ArgumentList.Arguments.Count != 1) + throw new CompilationException(left.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {left.ArgumentList.Arguments}"); + if (model.GetSymbolInfo(left).Symbol is IPropertySymbol property) + { + ConvertExpression(model, left.Expression); + ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + Call(model, property.GetMethod!, CallingConvention.StdCall); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + Call(model, property.SetMethod!, CallingConvention.Cdecl); + } + else + { + ConvertExpression(model, left.Expression); + ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.PICKITEM); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + AddInstruction(OpCode.REVERSE3); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IdentifierNameSyntax left, ExpressionSyntax right) + { + ISymbol symbol = model.GetSymbolInfo(left).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + ConvertFieldIdentifierNameComplexAssignment(model, type, operatorToken, field, right); + break; + case ILocalSymbol local: + ConvertLocalIdentifierNameComplexAssignment(model, type, operatorToken, local, right); + break; + case IParameterSymbol parameter: + ConvertParameterIdentifierNameComplexAssignment(model, type, operatorToken, parameter, right); + break; + case IPropertySymbol property: + ConvertPropertyIdentifierNameComplexAssignment(model, type, operatorToken, property, right); + break; + default: + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertMemberAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, MemberAccessExpressionSyntax left, ExpressionSyntax right) + { + ISymbol symbol = model.GetSymbolInfo(left).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + ConvertFieldMemberAccessComplexAssignment(model, type, operatorToken, left, right, field); + break; + case IPropertySymbol property: + ConvertPropertyMemberAccessComplexAssignment(model, type, operatorToken, left, right, property); + break; + default: + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertFieldIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IFieldSymbol left, ExpressionSyntax right) + { + if (left.IsStatic) + { + byte index = context.AddStaticField(left); + AccessSlot(OpCode.LDSFLD, index); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(left.ContainingType.GetFields(), left); + AddInstruction(OpCode.LDARG0); + AddInstruction(OpCode.DUP); + Push(index); + AddInstruction(OpCode.PICKITEM); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.TUCK); + Push(index); + AddInstruction(OpCode.SWAP); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertLocalIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, ILocalSymbol left, ExpressionSyntax right) + { + byte index = _localVariables[left]; + AccessSlot(OpCode.LDLOC, index); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STLOC, index); + } + + private void ConvertParameterIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IParameterSymbol left, ExpressionSyntax right) + { + byte index = _parameters[left]; + AccessSlot(OpCode.LDARG, index); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STARG, index); + } + + private void ConvertPropertyIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IPropertySymbol left, ExpressionSyntax right) + { + if (left.IsStatic) + { + Call(model, left.GetMethod!); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + Call(model, left.SetMethod!); + } + else + { + AddInstruction(OpCode.LDARG0); + AddInstruction(OpCode.DUP); + Call(model, left.GetMethod!); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.TUCK); + Call(model, left.SetMethod!, CallingConvention.StdCall); + } + } + + private void ConvertFieldMemberAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, MemberAccessExpressionSyntax left, ExpressionSyntax right, IFieldSymbol field) + { + if (field.IsStatic) + { + byte index = context.AddStaticField(field); + AccessSlot(OpCode.LDSFLD, index); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(field.ContainingType.GetFields(), field); + ConvertExpression(model, left.Expression); + AddInstruction(OpCode.DUP); + Push(index); + AddInstruction(OpCode.PICKITEM); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.TUCK); + Push(index); + AddInstruction(OpCode.SWAP); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertPropertyMemberAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, MemberAccessExpressionSyntax left, ExpressionSyntax right, IPropertySymbol property) + { + if (property.IsStatic) + { + Call(model, property.GetMethod!); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.DUP); + Call(model, property.SetMethod!); + } + else + { + ConvertExpression(model, left.Expression); + AddInstruction(OpCode.DUP); + Call(model, property.GetMethod!); + ConvertExpression(model, right); + EmitComplexAssignmentOperator(type, operatorToken); + AddInstruction(OpCode.TUCK); + Call(model, property.SetMethod!, CallingConvention.StdCall); + } + } + + private void EmitComplexAssignmentOperator(ITypeSymbol type, SyntaxToken operatorToken) + { + var itemType = type.GetStackItemType(); + bool isBoolean = itemType == VM.Types.StackItemType.Boolean; + bool isString = itemType == VM.Types.StackItemType.ByteString; + + var (opcode, checkResult) = operatorToken.ValueText switch + { + "+=" => isString ? (OpCode.CAT, false) : (OpCode.ADD, true), + "-=" => (OpCode.SUB, true), + "*=" => (OpCode.MUL, true), + "/=" => (OpCode.DIV, true), + "%=" => (OpCode.MOD, true), + "&=" => isBoolean ? (OpCode.BOOLAND, false) : (OpCode.AND, true), + "^=" when !isBoolean => (OpCode.XOR, true), + "|=" => isBoolean ? (OpCode.BOOLOR, false) : (OpCode.OR, true), + "<<=" => (OpCode.SHL, true), + ">>=" => (OpCode.SHR, true), + _ => throw new CompilationException(operatorToken, DiagnosticId.SyntaxNotSupported, $"Unsupported operator: {operatorToken}") + }; + AddInstruction(opcode); + if (isString) ChangeType(VM.Types.StackItemType.ByteString); + if (checkResult) EnsureIntegerInRange(type); + } +} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs new file mode 100644 index 000000000..7e0fd822a --- /dev/null +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.SimpleAssignment.cs @@ -0,0 +1,197 @@ +// Copyright (C) 2015-2023 The Neo Project. +// +// The Neo.Compiler.CSharp is free software distributed under the MIT +// software license, see the accompanying file LICENSE in the main directory +// of the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +extern alias scfx; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.VM; +using System; +using System.Runtime.InteropServices; + +namespace Neo.Compiler; + +partial class MethodConvert +{ + + private void ConvertSimpleAssignmentExpression(SemanticModel model, AssignmentExpressionSyntax expression) + { + ConvertExpression(model, expression.Right); + AddInstruction(OpCode.DUP); + switch (expression.Left) + { + case DeclarationExpressionSyntax left: + ConvertDeclarationAssignment(model, left); + break; + case ElementAccessExpressionSyntax left: + ConvertElementAccessAssignment(model, left); + break; + case IdentifierNameSyntax left: + ConvertIdentifierNameAssignment(model, left); + break; + case MemberAccessExpressionSyntax left: + ConvertMemberAccessAssignment(model, left); + break; + case TupleExpressionSyntax left: + ConvertTupleAssignment(model, left); + break; + default: + throw new CompilationException(expression.Left, DiagnosticId.SyntaxNotSupported, + $"Unsupported assignment: {expression.Left}"); + } + } + + private void ConvertDeclarationAssignment(SemanticModel model, DeclarationExpressionSyntax left) + { + ITypeSymbol type = model.GetTypeInfo(left).Type!; + if (!type.IsValueType) + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment type: {type}"); + AddInstruction(OpCode.UNPACK); + AddInstruction(OpCode.DROP); + foreach (VariableDesignationSyntax variable in ((ParenthesizedVariableDesignationSyntax)left.Designation).Variables) + { + switch (variable) + { + case SingleVariableDesignationSyntax singleVariableDesignation: + ILocalSymbol local = (ILocalSymbol)model.GetDeclaredSymbol(singleVariableDesignation)!; + byte index = AddLocalVariable(local); + AccessSlot(OpCode.STLOC, index); + break; + case DiscardDesignationSyntax: + AddInstruction(OpCode.DROP); + break; + default: + throw new CompilationException(variable, DiagnosticId.SyntaxNotSupported, $"Unsupported designation: {variable}"); + } + } + } + + private void ConvertElementAccessAssignment(SemanticModel model, ElementAccessExpressionSyntax left) + { + if (left.ArgumentList.Arguments.Count != 1) + throw new CompilationException(left.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {left.ArgumentList.Arguments}"); + if (model.GetSymbolInfo(left).Symbol is IPropertySymbol property) + { + ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); + ConvertExpression(model, left.Expression); + Call(model, property.SetMethod!, CallingConvention.Cdecl); + } + else + { + ConvertExpression(model, left.Expression); + ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.ROT); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertIdentifierNameAssignment(SemanticModel model, IdentifierNameSyntax left) + { + ISymbol symbol = model.GetSymbolInfo(left).Symbol!; + switch (symbol) + { + case IDiscardSymbol: + AddInstruction(OpCode.DROP); + break; + case IFieldSymbol field: + if (field.IsStatic) + { + byte index = context.AddStaticField(field); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(field.ContainingType.GetFields(), field); + AddInstruction(OpCode.LDARG0); + Push(index); + AddInstruction(OpCode.ROT); + AddInstruction(OpCode.SETITEM); + } + break; + case ILocalSymbol local: + AccessSlot(OpCode.STLOC, _localVariables[local]); + break; + case IParameterSymbol parameter: + AccessSlot(OpCode.STARG, _parameters[parameter]); + break; + case IPropertySymbol property: + if (!property.IsStatic) AddInstruction(OpCode.LDARG0); + Call(model, property.SetMethod!, CallingConvention.Cdecl); + break; + default: + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertMemberAccessAssignment(SemanticModel model, MemberAccessExpressionSyntax left) + { + ISymbol symbol = model.GetSymbolInfo(left.Name).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + if (field.IsStatic) + { + byte index = context.AddStaticField(field); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(field.ContainingType.GetFields(), field); + ConvertExpression(model, left.Expression); + Push(index); + AddInstruction(OpCode.ROT); + AddInstruction(OpCode.SETITEM); + } + break; + case IPropertySymbol property: + if (!property.IsStatic) ConvertExpression(model, left.Expression); + Call(model, property.SetMethod!, CallingConvention.Cdecl); + break; + default: + throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertTupleAssignment(SemanticModel model, TupleExpressionSyntax left) + { + AddInstruction(OpCode.UNPACK); + AddInstruction(OpCode.DROP); + foreach (ArgumentSyntax argument in left.Arguments) + { + switch (argument.Expression) + { + case DeclarationExpressionSyntax declaration: + switch (declaration.Designation) + { + case SingleVariableDesignationSyntax singleVariableDesignation: + ILocalSymbol local = (ILocalSymbol)model.GetDeclaredSymbol(singleVariableDesignation)!; + byte index = AddLocalVariable(local); + AccessSlot(OpCode.STLOC, index); + break; + case DiscardDesignationSyntax: + AddInstruction(OpCode.DROP); + break; + default: + throw new CompilationException(argument, DiagnosticId.SyntaxNotSupported, $"Unsupported designation: {argument}"); + } + break; + case IdentifierNameSyntax identifier: + ConvertIdentifierNameAssignment(model, identifier); + break; + case MemberAccessExpressionSyntax memberAccess: + ConvertMemberAccessAssignment(model, memberAccess); + break; + default: + throw new CompilationException(argument, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment: {argument}"); + } + } + } +} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.cs index cfe6b5672..026ef3eff 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/AssignmentExpression.cs @@ -11,11 +11,7 @@ extern alias scfx; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Neo.VM; -using System; -using System.Runtime.InteropServices; namespace Neo.Compiler; @@ -36,686 +32,4 @@ private void ConvertAssignmentExpression(SemanticModel model, AssignmentExpressi break; } } - - private void ConvertSimpleAssignmentExpression(SemanticModel model, AssignmentExpressionSyntax expression) - { - ConvertExpression(model, expression.Right); - AddInstruction(OpCode.DUP); - switch (expression.Left) - { - case DeclarationExpressionSyntax left: - ConvertDeclarationAssignment(model, left); - break; - case ElementAccessExpressionSyntax left: - ConvertElementAccessAssignment(model, left); - break; - case IdentifierNameSyntax left: - ConvertIdentifierNameAssignment(model, left); - break; - case MemberAccessExpressionSyntax left: - ConvertMemberAccessAssignment(model, left); - break; - case TupleExpressionSyntax left: - ConvertTupleAssignment(model, left); - break; - default: - throw new CompilationException(expression.Left, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment: {expression.Left}"); - } - } - - private void ConvertDeclarationAssignment(SemanticModel model, DeclarationExpressionSyntax left) - { - ITypeSymbol type = model.GetTypeInfo(left).Type!; - if (!type.IsValueType) - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment type: {type}"); - AddInstruction(OpCode.UNPACK); - AddInstruction(OpCode.DROP); - foreach (VariableDesignationSyntax variable in ((ParenthesizedVariableDesignationSyntax)left.Designation).Variables) - { - switch (variable) - { - case SingleVariableDesignationSyntax singleVariableDesignation: - ILocalSymbol local = (ILocalSymbol)model.GetDeclaredSymbol(singleVariableDesignation)!; - byte index = AddLocalVariable(local); - AccessSlot(OpCode.STLOC, index); - break; - case DiscardDesignationSyntax: - AddInstruction(OpCode.DROP); - break; - default: - throw new CompilationException(variable, DiagnosticId.SyntaxNotSupported, $"Unsupported designation: {variable}"); - } - } - } - - private void ConvertElementAccessAssignment(SemanticModel model, ElementAccessExpressionSyntax left) - { - if (left.ArgumentList.Arguments.Count != 1) - throw new CompilationException(left.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {left.ArgumentList.Arguments}"); - if (model.GetSymbolInfo(left).Symbol is IPropertySymbol property) - { - ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); - ConvertExpression(model, left.Expression); - Call(model, property.SetMethod!, CallingConvention.Cdecl); - } - else - { - ConvertExpression(model, left.Expression); - ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.ROT); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertIdentifierNameAssignment(SemanticModel model, IdentifierNameSyntax left) - { - ISymbol symbol = model.GetSymbolInfo(left).Symbol!; - switch (symbol) - { - case IDiscardSymbol: - AddInstruction(OpCode.DROP); - break; - case IFieldSymbol field: - if (field.IsStatic) - { - byte index = context.AddStaticField(field); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(field.ContainingType.GetFields(), field); - AddInstruction(OpCode.LDARG0); - Push(index); - AddInstruction(OpCode.ROT); - AddInstruction(OpCode.SETITEM); - } - break; - case ILocalSymbol local: - AccessSlot(OpCode.STLOC, _localVariables[local]); - break; - case IParameterSymbol parameter: - AccessSlot(OpCode.STARG, _parameters[parameter]); - break; - case IPropertySymbol property: - if (!property.IsStatic) AddInstruction(OpCode.LDARG0); - Call(model, property.SetMethod!, CallingConvention.Cdecl); - break; - default: - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertMemberAccessAssignment(SemanticModel model, MemberAccessExpressionSyntax left) - { - ISymbol symbol = model.GetSymbolInfo(left.Name).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - if (field.IsStatic) - { - byte index = context.AddStaticField(field); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(field.ContainingType.GetFields(), field); - ConvertExpression(model, left.Expression); - Push(index); - AddInstruction(OpCode.ROT); - AddInstruction(OpCode.SETITEM); - } - break; - case IPropertySymbol property: - if (!property.IsStatic) ConvertExpression(model, left.Expression); - Call(model, property.SetMethod!, CallingConvention.Cdecl); - break; - default: - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertTupleAssignment(SemanticModel model, TupleExpressionSyntax left) - { - AddInstruction(OpCode.UNPACK); - AddInstruction(OpCode.DROP); - foreach (ArgumentSyntax argument in left.Arguments) - { - switch (argument.Expression) - { - case DeclarationExpressionSyntax declaration: - switch (declaration.Designation) - { - case SingleVariableDesignationSyntax singleVariableDesignation: - ILocalSymbol local = (ILocalSymbol)model.GetDeclaredSymbol(singleVariableDesignation)!; - byte index = AddLocalVariable(local); - AccessSlot(OpCode.STLOC, index); - break; - case DiscardDesignationSyntax: - AddInstruction(OpCode.DROP); - break; - default: - throw new CompilationException(argument, DiagnosticId.SyntaxNotSupported, $"Unsupported designation: {argument}"); - } - break; - case IdentifierNameSyntax identifier: - ConvertIdentifierNameAssignment(model, identifier); - break; - case MemberAccessExpressionSyntax memberAccess: - ConvertMemberAccessAssignment(model, memberAccess); - break; - default: - throw new CompilationException(argument, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment: {argument}"); - } - } - } - - private void ConvertCoalesceAssignmentExpression(SemanticModel model, AssignmentExpressionSyntax expression) - { - switch (expression.Left) - { - case ElementAccessExpressionSyntax left: - ConvertElementAccessCoalesceAssignment(model, left, expression.Right); - break; - case IdentifierNameSyntax left: - ConvertIdentifierNameCoalesceAssignment(model, left, expression.Right); - break; - case MemberAccessExpressionSyntax left: - ConvertMemberAccessCoalesceAssignment(model, left, expression.Right); - break; - default: - throw new CompilationException(expression, DiagnosticId.SyntaxNotSupported, $"Unsupported coalesce assignment: {expression}"); - } - } - - private void ConvertElementAccessCoalesceAssignment(SemanticModel model, ElementAccessExpressionSyntax left, ExpressionSyntax right) - { - if (left.ArgumentList.Arguments.Count != 1) - throw new CompilationException(left.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {left.ArgumentList.Arguments}"); - JumpTarget assignmentTarget = new(); - JumpTarget endTarget = new(); - if (model.GetSymbolInfo(left).Symbol is IPropertySymbol property) - { - ConvertExpression(model, left.Expression); - ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - Call(model, property.GetMethod!, CallingConvention.StdCall); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AddInstruction(OpCode.NIP); - AddInstruction(OpCode.NIP); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.DROP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - Call(model, property.SetMethod!, CallingConvention.Cdecl); - } - else - { - ConvertExpression(model, left.Expression); - ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.PICKITEM); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AddInstruction(OpCode.PICKITEM); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - AddInstruction(OpCode.REVERSE3); - AddInstruction(OpCode.SETITEM); - } - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertIdentifierNameCoalesceAssignment(SemanticModel model, IdentifierNameSyntax left, ExpressionSyntax right) - { - ISymbol symbol = model.GetSymbolInfo(left).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - ConvertFieldIdentifierNameCoalesceAssignment(model, field, right); - break; - case ILocalSymbol local: - ConvertLocalIdentifierNameCoalesceAssignment(model, local, right); - break; - case IParameterSymbol parameter: - ConvertParameterIdentifierNameCoalesceAssignment(model, parameter, right); - break; - case IPropertySymbol property: - ConvertPropertyIdentifierNameCoalesceAssignment(model, property, right); - break; - default: - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertFieldIdentifierNameCoalesceAssignment(SemanticModel model, IFieldSymbol left, ExpressionSyntax right) - { - JumpTarget assignmentTarget = new(); - JumpTarget endTarget = new(); - if (left.IsStatic) - { - byte index = context.AddStaticField(left); - AccessSlot(OpCode.LDSFLD, index); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AccessSlot(OpCode.LDSFLD, index); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(left.ContainingType.GetFields(), left); - AddInstruction(OpCode.LDARG0); - Push(index); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.PICKITEM); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AddInstruction(OpCode.PICKITEM); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - AddInstruction(OpCode.REVERSE3); - AddInstruction(OpCode.SETITEM); - } - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertLocalIdentifierNameCoalesceAssignment(SemanticModel model, ILocalSymbol left, ExpressionSyntax right) - { - JumpTarget assignmentTarget = new(); - JumpTarget endTarget = new(); - byte index = _localVariables[left]; - AccessSlot(OpCode.LDLOC, index); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AccessSlot(OpCode.LDLOC, index); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STLOC, index); - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertParameterIdentifierNameCoalesceAssignment(SemanticModel model, IParameterSymbol left, ExpressionSyntax right) - { - JumpTarget assignmentTarget = new(); - JumpTarget endTarget = new(); - byte index = _parameters[left]; - AccessSlot(OpCode.LDARG, index); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AccessSlot(OpCode.LDARG, index); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STARG, index); - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertPropertyIdentifierNameCoalesceAssignment(SemanticModel model, IPropertySymbol left, ExpressionSyntax right) - { - JumpTarget endTarget = new(); - if (left.IsStatic) - { - Call(model, left.GetMethod!); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIFNOT_L, endTarget); - AddInstruction(OpCode.DROP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - Call(model, left.SetMethod!); - } - else - { - AddInstruction(OpCode.LDARG0); - Call(model, left.GetMethod!); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIFNOT_L, endTarget); - AddInstruction(OpCode.DROP); - AddInstruction(OpCode.LDARG0); - ConvertExpression(model, right); - AddInstruction(OpCode.TUCK); - Call(model, left.SetMethod!, CallingConvention.StdCall); - } - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertMemberAccessCoalesceAssignment(SemanticModel model, MemberAccessExpressionSyntax left, ExpressionSyntax right) - { - ISymbol symbol = model.GetSymbolInfo(left).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - ConvertFieldMemberAccessCoalesceAssignment(model, left, right, field); - break; - case IPropertySymbol property: - ConvertPropertyMemberAccessCoalesceAssignment(model, left, right, property); - break; - default: - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertFieldMemberAccessCoalesceAssignment(SemanticModel model, MemberAccessExpressionSyntax left, ExpressionSyntax right, IFieldSymbol field) - { - JumpTarget assignmentTarget = new(); - JumpTarget endTarget = new(); - if (field.IsStatic) - { - byte index = context.AddStaticField(field); - AccessSlot(OpCode.LDSFLD, index); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AccessSlot(OpCode.LDSFLD, index); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(field.ContainingType.GetFields(), field); - ConvertExpression(model, left.Expression); - Push(index); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.PICKITEM); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AddInstruction(OpCode.PICKITEM); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.NOP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - AddInstruction(OpCode.REVERSE3); - AddInstruction(OpCode.SETITEM); - } - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertPropertyMemberAccessCoalesceAssignment(SemanticModel model, MemberAccessExpressionSyntax left, ExpressionSyntax right, IPropertySymbol property) - { - JumpTarget endTarget = new(); - if (property.IsStatic) - { - Call(model, property.GetMethod!); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIFNOT_L, endTarget); - AddInstruction(OpCode.DROP); - ConvertExpression(model, right); - AddInstruction(OpCode.DUP); - Call(model, property.SetMethod!); - } - else - { - JumpTarget assignmentTarget = new(); - ConvertExpression(model, left.Expression); - AddInstruction(OpCode.DUP); - Call(model, property.GetMethod!); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.ISNULL); - Jump(OpCode.JMPIF_L, assignmentTarget); - AddInstruction(OpCode.NIP); - Jump(OpCode.JMP_L, endTarget); - assignmentTarget.Instruction = AddInstruction(OpCode.DROP); - ConvertExpression(model, right); - AddInstruction(OpCode.TUCK); - Call(model, property.SetMethod!, CallingConvention.StdCall); - } - endTarget.Instruction = AddInstruction(OpCode.NOP); - } - - private void ConvertComplexAssignmentExpression(SemanticModel model, AssignmentExpressionSyntax expression) - { - ITypeSymbol type = model.GetTypeInfo(expression).Type!; - switch (expression.Left) - { - case ElementAccessExpressionSyntax left: - ConvertElementAccessComplexAssignment(model, type, expression.OperatorToken, left, expression.Right); - break; - case IdentifierNameSyntax left: - ConvertIdentifierNameComplexAssignment(model, type, expression.OperatorToken, left, expression.Right); - break; - case MemberAccessExpressionSyntax left: - ConvertMemberAccessComplexAssignment(model, type, expression.OperatorToken, left, expression.Right); - break; - default: - throw new CompilationException(expression.Left, DiagnosticId.SyntaxNotSupported, $"Unsupported assignment expression: {expression}"); - } - } - - private void ConvertElementAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, ElementAccessExpressionSyntax left, ExpressionSyntax right) - { - if (left.ArgumentList.Arguments.Count != 1) - throw new CompilationException(left.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {left.ArgumentList.Arguments}"); - if (model.GetSymbolInfo(left).Symbol is IPropertySymbol property) - { - ConvertExpression(model, left.Expression); - ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - Call(model, property.GetMethod!, CallingConvention.StdCall); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - Call(model, property.SetMethod!, CallingConvention.Cdecl); - } - else - { - ConvertExpression(model, left.Expression); - ConvertExpression(model, left.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.PICKITEM); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - AddInstruction(OpCode.REVERSE3); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IdentifierNameSyntax left, ExpressionSyntax right) - { - ISymbol symbol = model.GetSymbolInfo(left).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - ConvertFieldIdentifierNameComplexAssignment(model, type, operatorToken, field, right); - break; - case ILocalSymbol local: - ConvertLocalIdentifierNameComplexAssignment(model, type, operatorToken, local, right); - break; - case IParameterSymbol parameter: - ConvertParameterIdentifierNameComplexAssignment(model, type, operatorToken, parameter, right); - break; - case IPropertySymbol property: - ConvertPropertyIdentifierNameComplexAssignment(model, type, operatorToken, property, right); - break; - default: - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertFieldIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IFieldSymbol left, ExpressionSyntax right) - { - if (left.IsStatic) - { - byte index = context.AddStaticField(left); - AccessSlot(OpCode.LDSFLD, index); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(left.ContainingType.GetFields(), left); - AddInstruction(OpCode.LDARG0); - AddInstruction(OpCode.DUP); - Push(index); - AddInstruction(OpCode.PICKITEM); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.TUCK); - Push(index); - AddInstruction(OpCode.SWAP); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertLocalIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, ILocalSymbol left, ExpressionSyntax right) - { - byte index = _localVariables[left]; - AccessSlot(OpCode.LDLOC, index); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STLOC, index); - } - - private void ConvertParameterIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IParameterSymbol left, ExpressionSyntax right) - { - byte index = _parameters[left]; - AccessSlot(OpCode.LDARG, index); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STARG, index); - } - - private void ConvertPropertyIdentifierNameComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, IPropertySymbol left, ExpressionSyntax right) - { - if (left.IsStatic) - { - Call(model, left.GetMethod!); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - Call(model, left.SetMethod!); - } - else - { - AddInstruction(OpCode.LDARG0); - AddInstruction(OpCode.DUP); - Call(model, left.GetMethod!); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.TUCK); - Call(model, left.SetMethod!, CallingConvention.StdCall); - } - } - - private void ConvertMemberAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, MemberAccessExpressionSyntax left, ExpressionSyntax right) - { - ISymbol symbol = model.GetSymbolInfo(left).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - ConvertFieldMemberAccessComplexAssignment(model, type, operatorToken, left, right, field); - break; - case IPropertySymbol property: - ConvertPropertyMemberAccessComplexAssignment(model, type, operatorToken, left, right, property); - break; - default: - throw new CompilationException(left, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertFieldMemberAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, MemberAccessExpressionSyntax left, ExpressionSyntax right, IFieldSymbol field) - { - if (field.IsStatic) - { - byte index = context.AddStaticField(field); - AccessSlot(OpCode.LDSFLD, index); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(field.ContainingType.GetFields(), field); - ConvertExpression(model, left.Expression); - AddInstruction(OpCode.DUP); - Push(index); - AddInstruction(OpCode.PICKITEM); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.TUCK); - Push(index); - AddInstruction(OpCode.SWAP); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertPropertyMemberAccessComplexAssignment(SemanticModel model, ITypeSymbol type, SyntaxToken operatorToken, MemberAccessExpressionSyntax left, ExpressionSyntax right, IPropertySymbol property) - { - if (property.IsStatic) - { - Call(model, property.GetMethod!); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.DUP); - Call(model, property.SetMethod!); - } - else - { - ConvertExpression(model, left.Expression); - AddInstruction(OpCode.DUP); - Call(model, property.GetMethod!); - ConvertExpression(model, right); - EmitComplexAssignmentOperator(type, operatorToken); - AddInstruction(OpCode.TUCK); - Call(model, property.SetMethod!, CallingConvention.StdCall); - } - } - - private void EmitComplexAssignmentOperator(ITypeSymbol type, SyntaxToken operatorToken) - { - var itemType = type.GetStackItemType(); - bool isBoolean = itemType == VM.Types.StackItemType.Boolean; - bool isString = itemType == VM.Types.StackItemType.ByteString; - - var (opcode, checkResult) = operatorToken.ValueText switch - { - "+=" => isString ? (OpCode.CAT, false) : (OpCode.ADD, true), - "-=" => (OpCode.SUB, true), - "*=" => (OpCode.MUL, true), - "/=" => (OpCode.DIV, true), - "%=" => (OpCode.MOD, true), - "&=" => isBoolean ? (OpCode.BOOLAND, false) : (OpCode.AND, true), - "^=" when !isBoolean => (OpCode.XOR, true), - "|=" => isBoolean ? (OpCode.BOOLOR, false) : (OpCode.OR, true), - "<<=" => (OpCode.SHL, true), - ">>=" => (OpCode.SHR, true), - _ => throw new CompilationException(operatorToken, DiagnosticId.SyntaxNotSupported, $"Unsupported operator: {operatorToken}") - }; - AddInstruction(opcode); - if (isString) ChangeType(VM.Types.StackItemType.ByteString); - if (checkResult) EnsureIntegerInRange(type); - } } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs new file mode 100644 index 000000000..f1826db21 --- /dev/null +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PostfixUnary.cs @@ -0,0 +1,235 @@ +// Copyright (C) 2015-2023 The Neo Project. +// +// The Neo.Compiler.CSharp is free software distributed under the MIT +// software license, see the accompanying file LICENSE in the main directory +// of the project or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +extern alias scfx; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.VM; +using System; +using System.Runtime.InteropServices; + +namespace Neo.Compiler; + +partial class MethodConvert +{ + private void ConvertPostfixUnaryExpression(SemanticModel model, PostfixUnaryExpressionSyntax expression) + { + switch (expression.OperatorToken.ValueText) + { + case "++": + case "--": + ConvertPostIncrementOrDecrementExpression(model, expression); + break; + case "!": + ConvertExpression(model, expression.Operand); + break; + default: + throw new CompilationException(expression.OperatorToken, DiagnosticId.SyntaxNotSupported, $"Unsupported operator: {expression.OperatorToken}"); + } + } + + private void ConvertPostIncrementOrDecrementExpression(SemanticModel model, PostfixUnaryExpressionSyntax expression) + { + switch (expression.Operand) + { + case ElementAccessExpressionSyntax operand: + ConvertElementAccessPostIncrementOrDecrementExpression(model, expression.OperatorToken, operand); + break; + case IdentifierNameSyntax operand: + ConvertIdentifierNamePostIncrementOrDecrementExpression(model, expression.OperatorToken, operand); + break; + case MemberAccessExpressionSyntax operand: + ConvertMemberAccessPostIncrementOrDecrementExpression(model, expression.OperatorToken, operand); + break; + default: + throw new CompilationException(expression, DiagnosticId.SyntaxNotSupported, $"Unsupported postfix unary expression: {expression}"); + } + } + + private void ConvertElementAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, ElementAccessExpressionSyntax operand) + { + if (operand.ArgumentList.Arguments.Count != 1) + throw new CompilationException(operand.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {operand.ArgumentList.Arguments}"); + if (model.GetSymbolInfo(operand).Symbol is IPropertySymbol property) + { + ConvertExpression(model, operand.Expression); + ConvertExpression(model, operand.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + Call(model, property.GetMethod!, CallingConvention.StdCall); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + AddInstruction(OpCode.REVERSE3); + EmitIncrementOrDecrement(operatorToken, property.Type); + Call(model, property.SetMethod!, CallingConvention.StdCall); + } + else + { + ConvertExpression(model, operand.Expression); + ConvertExpression(model, operand.ArgumentList.Arguments[0].Expression); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.OVER); + AddInstruction(OpCode.PICKITEM); + AddInstruction(OpCode.DUP); + AddInstruction(OpCode.REVERSE4); + AddInstruction(OpCode.REVERSE3); + EmitIncrementOrDecrement(operatorToken, model.GetTypeInfo(operand).Type); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertIdentifierNamePostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, IdentifierNameSyntax operand) + { + ISymbol symbol = model.GetSymbolInfo(operand).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + ConvertFieldIdentifierNamePostIncrementOrDecrementExpression(operatorToken, field); + break; + case ILocalSymbol local: + ConvertLocalIdentifierNamePostIncrementOrDecrementExpression(operatorToken, local); + break; + case IParameterSymbol parameter: + ConvertParameterIdentifierNamePostIncrementOrDecrementExpression(operatorToken, parameter); + break; + case IPropertySymbol property: + ConvertPropertyIdentifierNamePostIncrementOrDecrementExpression(model, operatorToken, property); + break; + default: + throw new CompilationException(operand, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertFieldIdentifierNamePostIncrementOrDecrementExpression(SyntaxToken operatorToken, IFieldSymbol symbol) + { + if (symbol.IsStatic) + { + byte index = context.AddStaticField(symbol); + AccessSlot(OpCode.LDSFLD, index); + AddInstruction(OpCode.DUP); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(symbol.ContainingType.GetFields(), symbol); + AddInstruction(OpCode.LDARG0); + AddInstruction(OpCode.DUP); + Push(index); + AddInstruction(OpCode.PICKITEM); + AddInstruction(OpCode.TUCK); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + Push(index); + AddInstruction(OpCode.SWAP); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertLocalIdentifierNamePostIncrementOrDecrementExpression(SyntaxToken operatorToken, ILocalSymbol symbol) + { + byte index = _localVariables[symbol]; + AccessSlot(OpCode.LDLOC, index); + AddInstruction(OpCode.DUP); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + AccessSlot(OpCode.STLOC, index); + } + + private void ConvertParameterIdentifierNamePostIncrementOrDecrementExpression(SyntaxToken operatorToken, IParameterSymbol symbol) + { + byte index = _parameters[symbol]; + AccessSlot(OpCode.LDARG, index); + AddInstruction(OpCode.DUP); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + AccessSlot(OpCode.STARG, index); + } + + private void ConvertPropertyIdentifierNamePostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, IPropertySymbol symbol) + { + if (symbol.IsStatic) + { + Call(model, symbol.GetMethod!); + AddInstruction(OpCode.DUP); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + Call(model, symbol.SetMethod!); + } + else + { + AddInstruction(OpCode.LDARG0); + AddInstruction(OpCode.DUP); + Call(model, symbol.GetMethod!); + AddInstruction(OpCode.TUCK); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + Call(model, symbol.SetMethod!, CallingConvention.StdCall); + } + } + + private void ConvertMemberAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, MemberAccessExpressionSyntax operand) + { + ISymbol symbol = model.GetSymbolInfo(operand).Symbol!; + switch (symbol) + { + case IFieldSymbol field: + ConvertFieldMemberAccessPostIncrementOrDecrementExpression(model, operatorToken, operand, field); + break; + case IPropertySymbol property: + ConvertPropertyMemberAccessPostIncrementOrDecrementExpression(model, operatorToken, operand, property); + break; + default: + throw new CompilationException(operand, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); + } + } + + private void ConvertFieldMemberAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, MemberAccessExpressionSyntax operand, IFieldSymbol symbol) + { + if (symbol.IsStatic) + { + byte index = context.AddStaticField(symbol); + AccessSlot(OpCode.LDSFLD, index); + AddInstruction(OpCode.DUP); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + AccessSlot(OpCode.STSFLD, index); + } + else + { + int index = Array.IndexOf(symbol.ContainingType.GetFields(), symbol); + ConvertExpression(model, operand.Expression); + AddInstruction(OpCode.DUP); + Push(index); + AddInstruction(OpCode.PICKITEM); + AddInstruction(OpCode.TUCK); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + Push(index); + AddInstruction(OpCode.SWAP); + AddInstruction(OpCode.SETITEM); + } + } + + private void ConvertPropertyMemberAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, MemberAccessExpressionSyntax operand, IPropertySymbol symbol) + { + if (symbol.IsStatic) + { + Call(model, symbol.GetMethod!); + AddInstruction(OpCode.DUP); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + Call(model, symbol.SetMethod!); + } + else + { + ConvertExpression(model, operand.Expression); + AddInstruction(OpCode.DUP); + Call(model, symbol.GetMethod!); + AddInstruction(OpCode.TUCK); + EmitIncrementOrDecrement(operatorToken, symbol.Type); + Call(model, symbol.SetMethod!, CallingConvention.StdCall); + } + } +} diff --git a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.cs b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs similarity index 54% rename from src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.cs rename to src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs index 74aeb537a..c22b766a3 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/Expression/UnaryExpression.PrefixUnary.cs @@ -21,218 +21,6 @@ namespace Neo.Compiler; partial class MethodConvert { - private void ConvertPostfixUnaryExpression(SemanticModel model, PostfixUnaryExpressionSyntax expression) - { - switch (expression.OperatorToken.ValueText) - { - case "++": - case "--": - ConvertPostIncrementOrDecrementExpression(model, expression); - break; - case "!": - ConvertExpression(model, expression.Operand); - break; - default: - throw new CompilationException(expression.OperatorToken, DiagnosticId.SyntaxNotSupported, $"Unsupported operator: {expression.OperatorToken}"); - } - } - - private void ConvertPostIncrementOrDecrementExpression(SemanticModel model, PostfixUnaryExpressionSyntax expression) - { - switch (expression.Operand) - { - case ElementAccessExpressionSyntax operand: - ConvertElementAccessPostIncrementOrDecrementExpression(model, expression.OperatorToken, operand); - break; - case IdentifierNameSyntax operand: - ConvertIdentifierNamePostIncrementOrDecrementExpression(model, expression.OperatorToken, operand); - break; - case MemberAccessExpressionSyntax operand: - ConvertMemberAccessPostIncrementOrDecrementExpression(model, expression.OperatorToken, operand); - break; - default: - throw new CompilationException(expression, DiagnosticId.SyntaxNotSupported, $"Unsupported postfix unary expression: {expression}"); - } - } - - private void ConvertElementAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, ElementAccessExpressionSyntax operand) - { - if (operand.ArgumentList.Arguments.Count != 1) - throw new CompilationException(operand.ArgumentList, DiagnosticId.MultidimensionalArray, $"Unsupported array rank: {operand.ArgumentList.Arguments}"); - if (model.GetSymbolInfo(operand).Symbol is IPropertySymbol property) - { - ConvertExpression(model, operand.Expression); - ConvertExpression(model, operand.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - Call(model, property.GetMethod!, CallingConvention.StdCall); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - AddInstruction(OpCode.REVERSE3); - EmitIncrementOrDecrement(operatorToken, property.Type); - Call(model, property.SetMethod!, CallingConvention.StdCall); - } - else - { - ConvertExpression(model, operand.Expression); - ConvertExpression(model, operand.ArgumentList.Arguments[0].Expression); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.OVER); - AddInstruction(OpCode.PICKITEM); - AddInstruction(OpCode.DUP); - AddInstruction(OpCode.REVERSE4); - AddInstruction(OpCode.REVERSE3); - EmitIncrementOrDecrement(operatorToken, model.GetTypeInfo(operand).Type); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertIdentifierNamePostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, IdentifierNameSyntax operand) - { - ISymbol symbol = model.GetSymbolInfo(operand).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - ConvertFieldIdentifierNamePostIncrementOrDecrementExpression(operatorToken, field); - break; - case ILocalSymbol local: - ConvertLocalIdentifierNamePostIncrementOrDecrementExpression(operatorToken, local); - break; - case IParameterSymbol parameter: - ConvertParameterIdentifierNamePostIncrementOrDecrementExpression(operatorToken, parameter); - break; - case IPropertySymbol property: - ConvertPropertyIdentifierNamePostIncrementOrDecrementExpression(model, operatorToken, property); - break; - default: - throw new CompilationException(operand, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertFieldIdentifierNamePostIncrementOrDecrementExpression(SyntaxToken operatorToken, IFieldSymbol symbol) - { - if (symbol.IsStatic) - { - byte index = context.AddStaticField(symbol); - AccessSlot(OpCode.LDSFLD, index); - AddInstruction(OpCode.DUP); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(symbol.ContainingType.GetFields(), symbol); - AddInstruction(OpCode.LDARG0); - AddInstruction(OpCode.DUP); - Push(index); - AddInstruction(OpCode.PICKITEM); - AddInstruction(OpCode.TUCK); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - Push(index); - AddInstruction(OpCode.SWAP); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertLocalIdentifierNamePostIncrementOrDecrementExpression(SyntaxToken operatorToken, ILocalSymbol symbol) - { - byte index = _localVariables[symbol]; - AccessSlot(OpCode.LDLOC, index); - AddInstruction(OpCode.DUP); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - AccessSlot(OpCode.STLOC, index); - } - - private void ConvertParameterIdentifierNamePostIncrementOrDecrementExpression(SyntaxToken operatorToken, IParameterSymbol symbol) - { - byte index = _parameters[symbol]; - AccessSlot(OpCode.LDARG, index); - AddInstruction(OpCode.DUP); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - AccessSlot(OpCode.STARG, index); - } - - private void ConvertPropertyIdentifierNamePostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, IPropertySymbol symbol) - { - if (symbol.IsStatic) - { - Call(model, symbol.GetMethod!); - AddInstruction(OpCode.DUP); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - Call(model, symbol.SetMethod!); - } - else - { - AddInstruction(OpCode.LDARG0); - AddInstruction(OpCode.DUP); - Call(model, symbol.GetMethod!); - AddInstruction(OpCode.TUCK); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - Call(model, symbol.SetMethod!, CallingConvention.StdCall); - } - } - - private void ConvertMemberAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, MemberAccessExpressionSyntax operand) - { - ISymbol symbol = model.GetSymbolInfo(operand).Symbol!; - switch (symbol) - { - case IFieldSymbol field: - ConvertFieldMemberAccessPostIncrementOrDecrementExpression(model, operatorToken, operand, field); - break; - case IPropertySymbol property: - ConvertPropertyMemberAccessPostIncrementOrDecrementExpression(model, operatorToken, operand, property); - break; - default: - throw new CompilationException(operand, DiagnosticId.SyntaxNotSupported, $"Unsupported symbol: {symbol}"); - } - } - - private void ConvertFieldMemberAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, MemberAccessExpressionSyntax operand, IFieldSymbol symbol) - { - if (symbol.IsStatic) - { - byte index = context.AddStaticField(symbol); - AccessSlot(OpCode.LDSFLD, index); - AddInstruction(OpCode.DUP); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - AccessSlot(OpCode.STSFLD, index); - } - else - { - int index = Array.IndexOf(symbol.ContainingType.GetFields(), symbol); - ConvertExpression(model, operand.Expression); - AddInstruction(OpCode.DUP); - Push(index); - AddInstruction(OpCode.PICKITEM); - AddInstruction(OpCode.TUCK); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - Push(index); - AddInstruction(OpCode.SWAP); - AddInstruction(OpCode.SETITEM); - } - } - - private void ConvertPropertyMemberAccessPostIncrementOrDecrementExpression(SemanticModel model, SyntaxToken operatorToken, MemberAccessExpressionSyntax operand, IPropertySymbol symbol) - { - if (symbol.IsStatic) - { - Call(model, symbol.GetMethod!); - AddInstruction(OpCode.DUP); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - Call(model, symbol.SetMethod!); - } - else - { - ConvertExpression(model, operand.Expression); - AddInstruction(OpCode.DUP); - Call(model, symbol.GetMethod!); - AddInstruction(OpCode.TUCK); - EmitIncrementOrDecrement(operatorToken, symbol.Type); - Call(model, symbol.SetMethod!, CallingConvention.StdCall); - } - } - private void ConvertPrefixUnaryExpression(SemanticModel model, PrefixUnaryExpressionSyntax expression) { switch (expression.OperatorToken.ValueText) diff --git a/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs b/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs index 80957e60c..5e0dd416d 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs @@ -967,9 +967,6 @@ private void ConvertExpression(SemanticModel model, ExpressionSyntax syntax) } } - - - private void EnsureIntegerInRange(ITypeSymbol type) { if (type.Name == "BigInteger") return; From 6d3784fe5c8d9f2a635ab8243595e4479fb70b78 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Fri, 16 Feb 2024 03:29:33 -0800 Subject: [PATCH 02/22] update neo (#906) * update neo * Remove init, and compile, now allow to change storage during executions --------- Co-authored-by: Fernando Diaz Toledano --- neo | 2 +- src/Neo.SmartContract.Testing/TestEngine.cs | 2 +- src/Neo.SmartContract.Testing/TestStorage.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/neo b/neo index fd3d68d52..3f002a657 160000 --- a/neo +++ b/neo @@ -1 +1 @@ -Subproject commit fd3d68d527322d150cd9d850440418ac7644c8b3 +Subproject commit 3f002a657bd7272f551eb6d4eafa5552cf0ac88a diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 57fada30f..e6a5ec171 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -71,7 +71,7 @@ public class TestEngine /// /// Storage /// - public TestStorage Storage { get; init; } = new TestStorage(new MemoryStore()); + public TestStorage Storage { get; set; } = new TestStorage(new MemoryStore()); /// /// Protocol Settings diff --git a/src/Neo.SmartContract.Testing/TestStorage.cs b/src/Neo.SmartContract.Testing/TestStorage.cs index 5919c25e5..d7b7fe8d9 100644 --- a/src/Neo.SmartContract.Testing/TestStorage.cs +++ b/src/Neo.SmartContract.Testing/TestStorage.cs @@ -17,7 +17,7 @@ public class TestStorage /// /// Store /// - public IStore Store { get; init; } = new MemoryStore(); + public IStore Store { get; } /// /// Snapshot From 932aba387dea4b0e83c0707f3b685d1141be0c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vitor=20Naz=C3=A1rio=20Coelho?= Date: Fri, 16 Feb 2024 21:02:48 -0300 Subject: [PATCH 03/22] Basic devcontainer (#907) * Basic devcontainer * Update devcontainer.json --- .devcontainer/devcontainer.json | 14 ++++++++++++++ .vscode/settings.json | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .vscode/settings.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..9acd809fc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,14 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + "image": "mcr.microsoft.com/devcontainers/dotnet:1-7.0-jammy", + "onCreateCommand": "./scripts/load_submodule.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csdevkit" + ] + } + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..872748a44 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "neo-devpack-dotnet.sln" +} \ No newline at end of file From 6ee55dba0f3c64eadd4e1f968818d35ac7469906 Mon Sep 17 00:00:00 2001 From: Shargon Date: Sat, 17 Feb 2024 01:09:03 +0100 Subject: [PATCH 04/22] Nep17 UnitTests (#893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Draft Nep17 UT * LF * Fix TotalSupply * Some UT still failing, checking bugs * Ensure OnSetOwner is raised during deploy * Fix string * Fix mint test * Test burn * check burn * Test transfer * Remove token reception in Nep17, not required for a template * Fix csproj * Try to execute template compilation * Update src/Neo.SmartContract.Template/templates/neocontractnep17/ProjectName.csproj * Rename project * Use same path as tested project * Fix ut contract=filename * fix comment * Update src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.cs * Need help * Create project with template, and compile, new errors * Generate nef and manifest before compile * Compile works * Only left #895 * forgot files * LF and remove artifact library * format * Update tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs * Remove artifacts * Remove artifacts compilation * Revert "Remove artifacts compilation" This reverts commit 62466f14c3bce3bb50ddcde1b50ccbf145963544. * Remove artifacts and use the compiled library * Remove duplicate neo reference in Compiler * Update Neo.Compiler.CSharp.csproj * With source works fine * Reduce changes * Ut pass in local * dotnet format * Prepare for coverage check * Update test with coverage * Fix git notifications * sort coverage dump * remove using * fix bug * Increase transfer coverage using invalid types * Add to workflow * Fix test order * Remove some artifacts * fix comment * ir require artifacts * Remove artifacts and generate them by workflow * Split build * fix path * fix path * fix nccs dll * fix relative * Fix artifacts path * clean project * Update README.md * Sandobx UT * Clean * remove nullable * cleaner * Update console log * Clean table * Update tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs Co-authored-by: Jimmy * Update src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs Co-authored-by: Christopher Schuchardt * cschuchardt88's and Jimmy's feedback & Ownable & Nep17UT * some fixes * Rename test * TestBase * Add Assert * Use same format for auth check * Fix typo * Update src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs Co-authored-by: Christopher Schuchardt * cschuchardt88's feedback * Wait for https://github.com/neo-project/neo/pull/3143 --------- Co-authored-by: Jimmy Co-authored-by: Christopher Schuchardt Co-authored-by: Vitor Nazário Coelho --- .github/workflows/main.yml | 24 ++- neo-devpack-dotnet.sln | 6 + .../Neo.Compiler.CSharp.csproj | 1 + src/Neo.Compiler.CSharp/Options.cs | 10 +- src/Neo.Compiler.CSharp/Program.cs | 98 +++++---- .../.template.config/template.json | 2 +- .../{Contract1.cs => Nep17Contract.cs} | 57 ++---- ...rojectName.csproj => Nep17Contract.csproj} | 0 .../Coverage/CoverageBase.cs | 2 +- .../Coverage/CoveredContract.cs | 105 +++++++--- .../Coverage/CoveredMethod.cs | 13 -- .../Extensions/ArtifactExtensions.cs | 125 +++++++++--- .../Extensions/StandardExtensions.cs | 47 +++++ .../Extensions/TestExtensions.cs | 76 +++---- .../InvalidTypes/InvalidUInt160.cs | 15 ++ .../InvalidTypes/InvalidUInt256.cs | 15 ++ .../Native/ContractManagement.cs | 38 +++- .../Native/CryptoLib.cs | 40 ++-- .../Native/GasToken.cs | 38 +++- .../Native/LedgerContract.cs | 41 +++- .../Native/NeoToken.cs | 122 +++++++++-- .../Native/OracleContract.cs | 28 ++- .../Native/PolicyContract.cs | 41 +++- .../Native/RoleManagement.cs | 21 +- .../Native/StdLib.cs | 69 ++++--- .../NativeArtifacts.cs | 2 +- .../Neo.SmartContract.Testing.csproj | 1 + src/Neo.SmartContract.Testing/README.md | 6 +- .../SmartContract.cs | 30 ++- src/Neo.SmartContract.Testing/TestEngine.cs | 48 +++-- .../TestingApplicationEngine.cs | 10 +- .../TestingStandards/INep17Standard.cs | 55 +++++ .../TestingStandards/IOwnable.cs | 24 +++ .../TestingStandards/IVerificable.cs | 11 + .../TestingStandards/Nep17Tests.cs | 156 ++++++++++++++ .../TestingStandards/OwnableTests.cs | 114 +++++++++++ .../TestingStandards/TestBase.cs | 46 +++++ ...eo.SmartContract.Template.UnitTests.csproj | 34 ++++ .../neocontractnep17/CoverageContractTests.cs | 27 +++ .../neocontractnep17/Nep17ContractTests.cs | 190 ++++++++++++++++++ .../neocontractnep17/OwnerContractTests.cs | 23 +++ .../Coverage/CoverageDataTests.cs | 58 +++--- .../Extensions/ArtifactExtensionsTests.cs | 76 +++++-- .../Extensions/TestExtensionsTests.cs | 19 ++ .../NativeArtifactsTests.cs | 6 +- .../TestEngineTests.cs | 2 +- 46 files changed, 1600 insertions(+), 372 deletions(-) rename src/Neo.SmartContract.Template/templates/neocontractnep17/{Contract1.cs => Nep17Contract.cs} (66%) rename src/Neo.SmartContract.Template/templates/neocontractnep17/{ProjectName.csproj => Nep17Contract.csproj} (100%) create mode 100644 src/Neo.SmartContract.Testing/Extensions/StandardExtensions.cs create mode 100644 src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt160.cs create mode 100644 src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt256.cs create mode 100644 src/Neo.SmartContract.Testing/TestingStandards/INep17Standard.cs create mode 100644 src/Neo.SmartContract.Testing/TestingStandards/IOwnable.cs create mode 100644 src/Neo.SmartContract.Testing/TestingStandards/IVerificable.cs create mode 100644 src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs create mode 100644 src/Neo.SmartContract.Testing/TestingStandards/OwnableTests.cs create mode 100644 src/Neo.SmartContract.Testing/TestingStandards/TestBase.cs create mode 100644 tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj create mode 100644 tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs create mode 100644 tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs create mode 100644 tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs create mode 100644 tests/Neo.SmartContract.Testing.UnitTests/Extensions/TestExtensionsTests.cs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 70360d161..389959aef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,18 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Build + - name: Build Neo.Compiler.CSharp + run: dotnet build ./src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj + - name: Build Neo.SmartContract.Template and generate artifacts + run: | + dotnet pack ./src/Neo.SmartContract.Template/Neo.SmartContract.Template.csproj + dotnet new install ./src/Neo.SmartContract.Template/bin/Debug/Neo.SmartContract.Template.*.nupkg + dotnet new neocontractnep17 -n Nep17Contract -o ./src/Neo.SmartContract.Template/bin/Debug/ --force + dotnet new uninstall Neo.SmartContract.Template + dotnet remove ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj package Neo.SmartContract.Framework + dotnet add ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj reference ./src/Neo.SmartContract.Framework/Neo.SmartContract.Framework.csproj + dotnet ./src/Neo.Compiler.CSharp/bin/Debug/net7.0/nccs.dll -d ./src/Neo.SmartContract.Template/bin/Debug/Nep17Contract.csproj -o ./tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Artifacts/ --generate-artifacts source + - name: Build Solution run: dotnet build ./neo-devpack-dotnet.sln - name: Check format run: | @@ -39,6 +50,17 @@ jobs: run: | dotnet test ./tests/Neo.SmartContract.Framework.UnitTests \ --no-build \ + -l "console;verbosity=normal" \ + -p:CollectCoverage=true \ + -p:CoverletOutput=${GITHUB_WORKSPACE}/coverage/lcov \ + -p:MergeWith=${GITHUB_WORKSPACE}/coverage/coverage.json \ + -p:Exclude=\"[Neo.Compiler.CSharp.UnitTests]*\" \ + -p:CoverletOutputFormat=lcov + - name: Test Neo.SmartContract.Template.UnitTests + run: | + dotnet test ./tests/Neo.SmartContract.Template.UnitTests \ + --no-build \ + -l "console;verbosity=detailed" \ -p:CollectCoverage=true \ -p:CoverletOutput=${GITHUB_WORKSPACE}/coverage/lcov \ -p:MergeWith=${GITHUB_WORKSPACE}/coverage/coverage.json \ diff --git a/neo-devpack-dotnet.sln b/neo-devpack-dotnet.sln index 02f55daa4..0ba3f1b26 100644 --- a/neo-devpack-dotnet.sln +++ b/neo-devpack-dotnet.sln @@ -36,6 +36,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Testing", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Testing.UnitTests", "tests\Neo.SmartContract.Testing.UnitTests\Neo.SmartContract.Testing.UnitTests.csproj", "{B772B8A9-9362-4C6F-A6D3-2A4138439B2C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.SmartContract.Template.UnitTests", "tests\Neo.SmartContract.Template.UnitTests\Neo.SmartContract.Template.UnitTests.csproj", "{17F45E0B-AB1C-4796-8C99-E5212A5592F8}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions", "neo\src\Neo.Extensions\Neo.Extensions.csproj", "{E5EFB018-810D-4297-8921-940FA0B1ED97}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.IO", "neo\src\Neo.IO\Neo.IO.csproj", "{C2B7927F-AAA5-432A-8E76-B5080BD7EFB9}" @@ -102,6 +103,10 @@ Global {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU {B772B8A9-9362-4C6F-A6D3-2A4138439B2C}.Release|Any CPU.Build.0 = Release|Any CPU + {17F45E0B-AB1C-4796-8C99-E5212A5592F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17F45E0B-AB1C-4796-8C99-E5212A5592F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17F45E0B-AB1C-4796-8C99-E5212A5592F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17F45E0B-AB1C-4796-8C99-E5212A5592F8}.Release|Any CPU.Build.0 = Release|Any CPU {E5EFB018-810D-4297-8921-940FA0B1ED97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E5EFB018-810D-4297-8921-940FA0B1ED97}.Debug|Any CPU.Build.0 = Debug|Any CPU {E5EFB018-810D-4297-8921-940FA0B1ED97}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -129,6 +134,7 @@ Global {D6D53889-5A10-46A4-BA66-E78B56EC1881} = {49D5873D-7B38-48A5-B853-85146F032091} {648DCE6F-A0BA-4032-951B-20CF5BBFD998} = {79389FC0-C621-4CEA-AD2B-6074C32E7BCA} {B772B8A9-9362-4C6F-A6D3-2A4138439B2C} = {D5266066-0AFD-44D5-A83E-2F73668A63C8} + {17F45E0B-AB1C-4796-8C99-E5212A5592F8} = {D5266066-0AFD-44D5-A83E-2F73668A63C8} {E5EFB018-810D-4297-8921-940FA0B1ED97} = {49D5873D-7B38-48A5-B853-85146F032091} {C2B7927F-AAA5-432A-8E76-B5080BD7EFB9} = {49D5873D-7B38-48A5-B853-85146F032091} EndGlobalSection diff --git a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj index 439137313..d88972126 100644 --- a/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj +++ b/src/Neo.Compiler.CSharp/Neo.Compiler.CSharp.csproj @@ -2,6 +2,7 @@ Neo.Compiler.CSharp + net7.0 nccs Exe Neo.Compiler.CSharp diff --git a/src/Neo.Compiler.CSharp/Options.cs b/src/Neo.Compiler.CSharp/Options.cs index a6df13210..204a6b5c5 100644 --- a/src/Neo.Compiler.CSharp/Options.cs +++ b/src/Neo.Compiler.CSharp/Options.cs @@ -16,13 +16,21 @@ namespace Neo.Compiler { public class Options { + public enum GenerateArtifactsKind + { + None, + Source, + Library, + SourceAndLibrary + } + public string? Output { get; set; } public string? BaseName { get; set; } public NullableContextOptions Nullable { get; set; } public bool Checked { get; set; } public bool Debug { get; set; } public bool Assembly { get; set; } - public bool NoArtifacts { get; set; } + public GenerateArtifactsKind GenerateArtifacts { get; set; } = GenerateArtifactsKind.Source; public bool NoOptimize { get; set; } public bool NoInline { get; set; } public byte AddressVersion { get; set; } diff --git a/src/Neo.Compiler.CSharp/Program.cs b/src/Neo.Compiler.CSharp/Program.cs index 3a95e6127..190cca02a 100644 --- a/src/Neo.Compiler.CSharp/Program.cs +++ b/src/Neo.Compiler.CSharp/Program.cs @@ -42,7 +42,7 @@ static int Main(string[] args) new Option("--checked", "Indicates whether to check for overflow and underflow."), new Option(new[] { "-d", "--debug" }, "Indicates whether to generate debugging information."), new Option("--assembly", "Indicates whether to generate assembly."), - new Option("--no-artifacts", "Instruct the compiler not to generate artifacts."), + new Option("--generate-artifacts", "Instruct the compiler how to generate artifacts."), new Option("--no-optimize", "Instruct the compiler not to optimize the code."), new Option("--no-inline", "Instruct the compiler not to insert inline code."), new Option("--address-version", () => ProtocolSettings.Default.AddressVersion, "Indicates the address version used by the compiler.") @@ -188,62 +188,74 @@ private static int ProcessOutputs(Options options, string folder, CompilationCon return 1; } Console.WriteLine($"Created {path}"); - if (!options.NoArtifacts) + + if (options.GenerateArtifacts != Options.GenerateArtifactsKind.None) { - var artifact = manifest.Abi.GetArtifactsSource(baseName); - path = Path.Combine(outputFolder, $"{baseName}.artifacts.cs"); - File.WriteAllText(path, artifact); - Console.WriteLine($"Created {path}"); + var artifact = manifest.GetArtifactsSource(baseName); - try + if (options.GenerateArtifacts == Options.GenerateArtifactsKind.SourceAndLibrary || options.GenerateArtifacts == Options.GenerateArtifactsKind.Source) { - // Try to compile the artifacts into a dll - - string coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + path = Path.Combine(outputFolder, $"{baseName}.artifacts.cs"); + File.WriteAllText(path, artifact); + Console.WriteLine($"Created {path}"); + } - var syntaxTree = CSharpSyntaxTree.ParseText(artifact); - var references = new MetadataReference[] + if (options.GenerateArtifacts == Options.GenerateArtifactsKind.SourceAndLibrary || options.GenerateArtifacts == Options.GenerateArtifactsKind.Library) + { + try { - MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")), - MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.InteropServices.dll")), - MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - MetadataReference.CreateFromFile(typeof(DisplayNameAttribute).Assembly.Location), - MetadataReference.CreateFromFile(typeof(System.Numerics.BigInteger).Assembly.Location), - MetadataReference.CreateFromFile(typeof(UInt160).Assembly.Location), - MetadataReference.CreateFromFile(typeof(SmartContract.Testing.SmartContract).Assembly.Location) - }; + // Try to compile the artifacts into a dll + + var coreDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var references = new MetadataReference[] + { + MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")), + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(DisplayNameAttribute).Assembly.Location), + MetadataReference.CreateFromFile(typeof(System.Numerics.BigInteger).Assembly.Location), + MetadataReference.CreateFromFile(typeof(NeoSystem).Assembly.Location), + MetadataReference.CreateFromFile(typeof(SmartContract.Testing.TestEngine).Assembly.Location) + }; - var compilation = CSharpCompilation.Create(baseName, new[] { syntaxTree }, references, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + CSharpCompilationOptions csOptions = new( + OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Debug, + platform: Platform.AnyCpu, + nullableContextOptions: NullableContextOptions.Enable, + deterministic: true); - using var ms = new MemoryStream(); - EmitResult result = compilation.Emit(ms); + var syntaxTree = CSharpSyntaxTree.ParseText(artifact, options: CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest)); + var compilation = CSharpCompilation.Create(baseName, new[] { syntaxTree }, references, csOptions); - if (!result.Success) - { - var failures = result.Diagnostics.Where(diagnostic => - diagnostic.IsWarningAsError || - diagnostic.Severity == DiagnosticSeverity.Error); + using var ms = new MemoryStream(); + EmitResult result = compilation.Emit(ms); - foreach (var diagnostic in failures) + if (!result.Success) { - Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); + var failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || + diagnostic.Severity == DiagnosticSeverity.Error); + + foreach (var diagnostic in failures) + { + Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); + } } - } - else - { - ms.Seek(0, SeekOrigin.Begin); + else + { + ms.Seek(0, SeekOrigin.Begin); - // Write dll + // Write dll - path = Path.Combine(outputFolder, $"{baseName}.artifacts.dll"); - File.WriteAllBytes(path, ms.ToArray()); - Console.WriteLine($"Created {path}"); + path = Path.Combine(outputFolder, $"{baseName}.artifacts.dll"); + File.WriteAllBytes(path, ms.ToArray()); + Console.WriteLine($"Created {path}"); + } + } + catch + { + Console.Error.WriteLine("Artifacts compilation error."); } - } - catch - { - Console.Error.WriteLine("Artifacts compilation error."); } } if (options.Debug) diff --git a/src/Neo.SmartContract.Template/templates/neocontractnep17/.template.config/template.json b/src/Neo.SmartContract.Template/templates/neocontractnep17/.template.config/template.json index 7a2a47621..b940f5e1c 100644 --- a/src/Neo.SmartContract.Template/templates/neocontractnep17/.template.config/template.json +++ b/src/Neo.SmartContract.Template/templates/neocontractnep17/.template.config/template.json @@ -10,7 +10,7 @@ "language": "C#", "type": "project" }, - "sourceName": "ProjectName", + "sourceName": "Nep17Contract", "symbols": { "NeoVersion": { "type": "parameter", diff --git a/src/Neo.SmartContract.Template/templates/neocontractnep17/Contract1.cs b/src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.cs similarity index 66% rename from src/Neo.SmartContract.Template/templates/neocontractnep17/Contract1.cs rename to src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.cs index 09c77719e..cf4a3b79c 100644 --- a/src/Neo.SmartContract.Template/templates/neocontractnep17/Contract1.cs +++ b/src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.cs @@ -10,15 +10,15 @@ namespace ProjectName { - [DisplayName(nameof(Contract1))] + [DisplayName(nameof(Nep17Contract))] [ManifestExtra("Author", "")] [ManifestExtra("Description", "")] [ManifestExtra("Email", "")] [ManifestExtra("Version", "")] - [ContractSourceCode("https://github.com/neo-project/neo-devpack-dotnet/tree/master/src/Neo.SmartContract.Template")] + [ContractSourceCode("https://github.com/neo-project/neo-devpack-dotnet/tree/master/src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.cs")] [ContractPermission("*", "*")] [SupportedStandards("NEP-17")] - public class Contract1 : Nep17Token + public class Nep17Contract : Nep17Token { #region Owner @@ -33,7 +33,7 @@ public static UInt160 GetOwner() private static bool IsOwner() => Runtime.CheckWitness(GetOwner()); - public delegate void OnSetOwnerDelegate(UInt160 newOwner); + public delegate void OnSetOwnerDelegate(UInt160 previousOwner, UInt160 newOwner); [DisplayName("SetOwner")] public static event OnSetOwnerDelegate OnSetOwner; @@ -45,8 +45,9 @@ public static void SetOwner(UInt160 newOwner) ExecutionEngine.Assert(newOwner.IsValid && !newOwner.IsZero, "owner must be valid"); + UInt160 previous = GetOwner(); Storage.Put(new[] { Prefix_Owner }, newOwner); - OnSetOwner(newOwner); + OnSetOwner(previous, newOwner); } #endregion @@ -75,40 +76,6 @@ public static void SetOwner(UInt160 newOwner) #endregion - #region Payment - - public static bool Withdraw(UInt160 token, UInt160 to, BigInteger amount) - { - if (IsOwner() == false) - throw new InvalidOperationException("No Authorization!"); - if (amount <= 0) - throw new ArgumentOutOfRangeException(nameof(amount)); - if (to == null || to.IsValid == false) - throw new ArgumentException("Invalid Address!"); - if (token == null || token.IsValid == false) - throw new ArgumentException("Invalid Token Address!"); - if (ContractManagement.GetContract(token) == null) - throw new ArgumentException("Token Not A Contract!"); - // TODO: Add logic - return true; - } - - // NOTE: Allows ALL NEP-17 tokens to be received for this contract - public static void OnNEP17Payment(UInt160 from, BigInteger amount, object data) - { - // TODO: Add logic for specific NEP-17 contract tokens - if (Runtime.CallingScriptHash == NEO.Hash) - { - // TODO: Add logic (Burn, Mint, Transfer, Etc) - } - if (Runtime.CallingScriptHash == GAS.Hash) - { - // TODO: Add logic (Burn, Mint, Transfer, Etc) - } - } - - #endregion - // When this contract address is included in the transaction signature, // this method will be triggered as a VerificationTrigger to verify that the signature is correct. // For example, this method needs to be called when withdrawing token from the contract. @@ -121,6 +88,7 @@ public static string MyMethod() return Storage.Get(Storage.CurrentContext, "Hello"); } + // This will be executed during deploy public static void _deploy(object data, bool update) { if (update) @@ -137,16 +105,15 @@ public static void _deploy(object data, bool update) ExecutionEngine.Assert(initialOwner.IsValid && !initialOwner.IsZero, "owner must exists"); Storage.Put(new[] { Prefix_Owner }, initialOwner); - OnSetOwner(initialOwner); - - // This will be executed during deploy + OnSetOwner(null, initialOwner); Storage.Put(Storage.CurrentContext, "Hello", "World"); } - public static void Update(ByteString nefFile, string manifest) + public static void Update(ByteString nefFile, string manifest, object data) { - if (!IsOwner()) throw new Exception("No authorization."); - ContractManagement.Update(nefFile, manifest, null); + if (IsOwner() == false) + throw new InvalidOperationException("No authorization."); + ContractManagement.Update(nefFile, manifest, data); } // NOTE: NEP-17 contracts "SHOULD NOT" have "Destroy" method diff --git a/src/Neo.SmartContract.Template/templates/neocontractnep17/ProjectName.csproj b/src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.csproj similarity index 100% rename from src/Neo.SmartContract.Template/templates/neocontractnep17/ProjectName.csproj rename to src/Neo.SmartContract.Template/templates/neocontractnep17/Nep17Contract.csproj diff --git a/src/Neo.SmartContract.Testing/Coverage/CoverageBase.cs b/src/Neo.SmartContract.Testing/Coverage/CoverageBase.cs index ed7cbc132..1920e0335 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoverageBase.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoverageBase.cs @@ -36,7 +36,7 @@ public float CoveredPercentage var total = TotalInstructions; if (total == 0) return 0F; - return (float)CoveredInstructions / total * 100F; + return (float)CoveredInstructions / total; } } diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs index 06e9e8a4b..c668a6bf6 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs @@ -14,10 +14,7 @@ public class CoveredContract : CoverageBase { #region Internal - /// - /// Coverage Data - /// - internal Dictionary CoverageData { get; } = new(); + private readonly Dictionary _coverageData = new(); #endregion @@ -29,12 +26,12 @@ public class CoveredContract : CoverageBase /// /// Methods /// - public CoveredMethod[] Methods { get; } + public CoveredMethod[] Methods { get; private set; } /// /// Coverage /// - public override IEnumerable Coverage => CoverageData.Values; + public override IEnumerable Coverage => _coverageData.Values; /// /// CoveredContract @@ -51,14 +48,7 @@ public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script) // Extract all methods - if (abi is not null) - { - Methods = abi.Methods - .Select(u => CreateMethod(abi, script, u)) - .Where(u => u is not null) - .OrderBy(u => u!.Offset) - .ToArray()!; - } + GenerateMethods(abi, script); // Iterate all valid instructions @@ -67,12 +57,24 @@ 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, false); ip += instruction.Size; } } - private CoveredMethod? CreateMethod(ContractAbi abi, Script script, ContractMethodDescriptor abiMethod) + internal void GenerateMethods(ContractAbi? abi, Script? script) + { + Methods = Array.Empty(); + + if (script is null || abi is null) return; + + Methods = abi.Methods + .Select(s => CreateMethod(abi, script, s)) + .OrderBy(o => o.Offset) + .ToArray()!; + } + + private CoveredMethod CreateMethod(ContractAbi abi, Script script, ContractMethodDescriptor abiMethod) { var to = script.Length - 1; var next = abi.Methods.OrderBy(u => u.Offset).Where(u => u.Offset > abiMethod.Offset).FirstOrDefault(); @@ -111,21 +113,26 @@ public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script) /// Join coverage /// /// Coverage - public void Join(IEnumerable coverage) + public void Join(IEnumerable? coverage) { + if (coverage is null || coverage.Any() == false) return; + // Join the coverage between them foreach (var c in coverage) { if (c.Hits == 0) continue; - if (CoverageData.TryGetValue(c.Offset, out var kvpValue)) - { - kvpValue.Hit(c); - } - else + lock (_coverageData) { - CoverageData.Add(c.Offset, c.Clone()); + if (_coverageData.TryGetValue(c.Offset, out var kvpValue)) + { + kvpValue.Hit(c); + } + else + { + _coverageData.Add(c.Offset, c.Clone()); + } } } } @@ -136,32 +143,64 @@ public void Join(IEnumerable coverage) /// Coverage dump public string Dump() { - // TODO: improve dump later - var builder = new StringBuilder(); using var sourceCode = new StringWriter(builder) { NewLine = "\n" }; - var cover = CoveredPercentage.ToString("0.00").ToString(); - sourceCode.WriteLine($"| {Hash,-50} | {cover,7}% |"); + var cover = $"{CoveredPercentage:P2}"; + sourceCode.WriteLine($"{Hash} [{cover}]"); - foreach (var method in Methods) + 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)) { - sourceCode.WriteLine(method.Dump()); + 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]); } + 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])} │"); + } + + sourceCode.WriteLine($"└-{"─".PadLeft(max[0], '─')}-┴-{"─".PadLeft(max[1], '─')}-┘"); + return builder.ToString(); } /// - /// String representation + /// Hit /// - /// - public override string ToString() + /// Instruction pointer + /// Gas + public void Hit(int instructionPointer, long gas) { - return $"Hash:{Hash}"; + lock (_coverageData) + { + if (!_coverageData.TryGetValue(instructionPointer, out var coverage)) + { + // Note: This call is unusual, out of the expected + + _coverageData[instructionPointer] = coverage = new CoverageHit(instructionPointer, true); + } + coverage.Hit(gas); + } } + + /// + /// String representation + /// + /// Hash + public override string ToString() => Hash.ToString(); } } diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs index 2dd1708b0..15a8b4276 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredMethod.cs @@ -46,19 +46,6 @@ public CoveredMethod(CoveredContract contract, ContractMethodDescriptor method, MethodLength = methodLength; } - /// - /// Dump coverage - /// - /// Coverage dump - public string Dump() - { - // TODO: improve dump later - - var cover = CoveredPercentage.ToString("0.00").ToString(); - - return $"| {Method,50} | {cover,7}% |"; - } - public override string ToString() => Method.ToString(); } } diff --git a/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs index 0b260af0b..27e3f6251 100644 --- a/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs +++ b/src/Neo.SmartContract.Testing/Extensions/ArtifactExtensions.cs @@ -1,4 +1,5 @@ using Neo.SmartContract.Manifest; +using Neo.SmartContract.Testing.TestingStandards; using System; using System.Collections.Generic; using System.IO; @@ -9,7 +10,8 @@ namespace Neo.SmartContract.Testing.Extensions { public static class ArtifactExtensions { - static readonly string[] _protectedWords = new string[] { + static readonly string[] _protectedWords = new string[] + { "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", "class", "const", "continue", "decimal", "default", "delegate", "do", "double", @@ -26,20 +28,31 @@ public static class ArtifactExtensions }; /// - /// Get source code from contract Abi + /// Get source code from contract Manifest /// - /// Abi - /// Contract name + /// Manifest + /// Class name, by default is manifest.Name /// Generate properties /// Source - public static string GetArtifactsSource(this ContractAbi abi, string name, bool generateProperties = true) + public static string GetArtifactsSource(this ContractManifest manifest, string? name = null, bool generateProperties = true) { + name ??= manifest.Name; + var builder = new StringBuilder(); using var sourceCode = new StringWriter(builder) { NewLine = "\n" }; + var inheritance = new List + { + typeof(SmartContract) + }; + + if (manifest.IsNep17()) inheritance.Add(typeof(INep17Standard)); + if (manifest.IsOwnable()) inheritance.Add(typeof(IOwnable)); + if (manifest.IsVerificable()) inheritance.Add(typeof(IVerificable)); + sourceCode.WriteLine("using Neo.Cryptography.ECC;"); sourceCode.WriteLine("using System.Collections.Generic;"); sourceCode.WriteLine("using System.ComponentModel;"); @@ -47,47 +60,54 @@ public static string GetArtifactsSource(this ContractAbi abi, string name, bool sourceCode.WriteLine(""); sourceCode.WriteLine("namespace Neo.SmartContract.Testing;"); sourceCode.WriteLine(""); - sourceCode.WriteLine($"public abstract class {name} : Neo.SmartContract.Testing.SmartContract"); + sourceCode.WriteLine($"public abstract class {name} : " + string.Join(", ", inheritance)); sourceCode.WriteLine("{"); // Crete events - if (abi.Events.Any()) + if (manifest.Abi.Events.Any()) { sourceCode.WriteLine(" #region Events"); + sourceCode.WriteLine(); - foreach (var ev in abi.Events.OrderBy(u => u.Name)) + foreach (var ev in manifest.Abi.Events.OrderBy(u => u.Name)) { - sourceCode.Write(CreateSourceEventFromManifest(ev)); + sourceCode.Write(CreateSourceEventFromManifest(ev, inheritance)); + sourceCode.WriteLine(); } sourceCode.WriteLine(" #endregion"); + sourceCode.WriteLine(); } // Create methods - var methods = abi.Methods; + var methods = manifest.Abi.Methods; if (generateProperties) { - (methods, var properties) = ProcessAbiMethods(abi.Methods); + (methods, var properties) = ProcessAbiMethods(manifest.Abi.Methods); if (properties.Any()) { sourceCode.WriteLine(" #region Properties"); + sourceCode.WriteLine(); foreach (var property in properties.OrderBy(u => u.getter.Name)) { sourceCode.Write(CreateSourcePropertyFromManifest(property.getter, property.setter)); + sourceCode.WriteLine(); } sourceCode.WriteLine(" #endregion"); + sourceCode.WriteLine(); } } if (methods.Any(u => u.Safe)) { sourceCode.WriteLine(" #region Safe methods"); + sourceCode.WriteLine(); foreach (var method in methods.Where(u => u.Safe).OrderBy(u => u.Name)) { @@ -96,14 +116,17 @@ public static string GetArtifactsSource(this ContractAbi abi, string name, bool if (method.Name.StartsWith("_")) continue; sourceCode.Write(CreateSourceMethodFromManifest(method)); + sourceCode.WriteLine(); } sourceCode.WriteLine(" #endregion"); + sourceCode.WriteLine(); } if (methods.Any(u => !u.Safe)) { sourceCode.WriteLine(" #region Unsafe methods"); + sourceCode.WriteLine(); foreach (var method in methods.Where(u => !u.Safe).OrderBy(u => u.Name)) { @@ -112,14 +135,18 @@ public static string GetArtifactsSource(this ContractAbi abi, string name, bool if (method.Name.StartsWith("_")) continue; sourceCode.Write(CreateSourceMethodFromManifest(method)); + sourceCode.WriteLine(); } sourceCode.WriteLine(" #endregion"); + sourceCode.WriteLine(); } // Create constructor sourceCode.WriteLine(" #region Constructor for internal use only"); + sourceCode.WriteLine(); sourceCode.WriteLine($" protected {name}(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) {{ }}"); + sourceCode.WriteLine(); sourceCode.WriteLine(" #endregion"); sourceCode.WriteLine("}"); @@ -165,18 +192,54 @@ private static (ContractMethodDescriptor[] methods, (ContractMethodDescriptor ge /// Create source code from event /// /// Event + /// Inheritance /// Source - private static string CreateSourceEventFromManifest(ContractEventDescriptor ev) + private static string CreateSourceEventFromManifest(ContractEventDescriptor ev, IList inheritance) { - var evName = TongleLowercase(EscapeName(ev.Name)); - if (!evName.StartsWith("On")) evName = "On" + evName; - var builder = new StringBuilder(); using var sourceCode = new StringWriter(builder) { NewLine = "\n" }; + switch (ev.Name) + { + case "Transfer": + { + if (inheritance.Contains(typeof(INep17Standard)) && ev.Parameters.Length == 3 && + ev.Parameters[0].Type == ContractParameterType.Hash160 && + ev.Parameters[1].Type == ContractParameterType.Hash160 && + ev.Parameters[2].Type == ContractParameterType.Integer) + { + sourceCode.WriteLine($" [DisplayName(\"{ev.Name}\")]"); + sourceCode.WriteLine(" public event Neo.SmartContract.Testing.TestingStandards.INep17Standard.delTransfer? OnTransfer;"); + return builder.ToString(); + } + + break; + } + case "SetOwner": + { + if (inheritance.Contains(typeof(IOwnable)) && ev.Parameters.Length == 2 && + ev.Parameters[0].Type == ContractParameterType.Hash160 && + ev.Parameters[1].Type == ContractParameterType.Hash160) + { + sourceCode.WriteLine($" [DisplayName(\"{ev.Name}\")]"); + sourceCode.WriteLine(" public event Neo.SmartContract.Testing.TestingStandards.IOwnable.delSetOwner? OnSetOwner;"); + return builder.ToString(); + } + + break; + } + } + + // Add On prefix + + var evName = TongleLowercase(EscapeName(ev.Name)); + if (!evName.StartsWith("On")) evName = "On" + evName; + + // Compose delegate + sourceCode.Write($" public delegate void del{ev.Name}("); var isFirst = true; @@ -189,6 +252,10 @@ private static string CreateSourceEventFromManifest(ContractEventDescriptor ev) } sourceCode.WriteLine(");"); + sourceCode.WriteLine(); + + // Compose event + if (ev.Name != evName) { sourceCode.WriteLine($" [DisplayName(\"{ev.Name}\")]"); @@ -214,6 +281,10 @@ private static string CreateSourcePropertyFromManifest(ContractMethodDescriptor { NewLine = "\n" }; + + sourceCode.WriteLine($" /// "); + sourceCode.WriteLine($" /// {(getter.Safe ? "Safe property" : "Unsafe property")}"); + sourceCode.WriteLine($" /// "); sourceCode.WriteLine($" public abstract {TypeToSource(getter.ReturnType)} {propertyName} {getset}"); return builder.ToString(); @@ -256,7 +327,7 @@ private static string CreateSourceMethodFromManifest(ContractMethodDescriptor me { // it will be object X, we can add a default value - sourceCode.Write($"{TypeToSource(arg.Type)}? {EscapeName(arg.Name)} = null"); + sourceCode.Write($"{TypeToSource(arg.Type)} {EscapeName(arg.Name)} = null"); } else { @@ -307,18 +378,18 @@ private static string TypeToSource(ContractParameterType type) { return type switch { - ContractParameterType.Boolean => "bool", - ContractParameterType.Integer => "BigInteger", - ContractParameterType.String => "string", - ContractParameterType.Hash160 => "UInt160", - ContractParameterType.Hash256 => "UInt256", - ContractParameterType.PublicKey => "ECPoint", - ContractParameterType.ByteArray => "byte[]", - ContractParameterType.Signature => "byte[]", - ContractParameterType.Array => "IList", - ContractParameterType.Map => "IDictionary", + ContractParameterType.Boolean => "bool?", + ContractParameterType.Integer => "BigInteger?", + ContractParameterType.String => "string?", + ContractParameterType.Hash160 => "UInt160?", + ContractParameterType.Hash256 => "UInt256?", + ContractParameterType.PublicKey => "ECPoint?", + ContractParameterType.ByteArray => "byte[]?", + ContractParameterType.Signature => "byte[]?", + ContractParameterType.Array => "IList?", + ContractParameterType.Map => "IDictionary?", ContractParameterType.Void => "void", - _ => "object", + _ => "object?", }; } } diff --git a/src/Neo.SmartContract.Testing/Extensions/StandardExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/StandardExtensions.cs new file mode 100644 index 000000000..565fd184c --- /dev/null +++ b/src/Neo.SmartContract.Testing/Extensions/StandardExtensions.cs @@ -0,0 +1,47 @@ +using Neo.SmartContract.Manifest; +using System; +using System.Linq; + +namespace Neo.SmartContract.Testing.Extensions +{ + public static class StandardExtensions + { + /// + /// Is Nep17 contract + /// + /// Manifest + /// True if NEP-17 + public static bool IsNep17(this ContractManifest manifest) + { + return manifest.SupportedStandards.Contains("NEP-17"); + } + + /// + /// Is Ownable + /// + /// Manifest + /// True if is Ownable + public static bool IsOwnable(this ContractManifest manifest) + { + return + manifest.Abi.Methods + .Any(u => u.Name == "getOwner" && u.Safe && u.Parameters.Length == 0) && + manifest.Abi.Methods + .Any(u => u.Name == "setOwner" && !u.Safe && u.Parameters.Length == 1 && u.Parameters[0].Type == ContractParameterType.Hash160) && + manifest.Abi.Events + .Any(u => u.Name == "SetOwner" && u.Parameters.Length == 2 && + u.Parameters[0].Type == ContractParameterType.Hash160 && + u.Parameters[1].Type == ContractParameterType.Hash160); + } + + /// + /// Is Verificable + /// + /// Manifest + /// True if is Verificable + public static bool IsVerificable(this ContractManifest manifest) + { + return manifest.Abi.Methods.Any(u => u.Name == "verify" && u.Safe && u.Parameters.Length == 0); + } + } +} diff --git a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs index 379927630..71f1c8a9c 100644 --- a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs +++ b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs @@ -1,4 +1,5 @@ -using Neo.IO; +using Neo.Cryptography.ECC; +using Neo.SmartContract.Iterators; using Neo.VM.Types; using System; using System.Collections.Generic; @@ -10,38 +11,6 @@ namespace Neo.SmartContract.Testing.Extensions { public static class TestExtensions { - /// - /// Convert dotnet type to stack item - /// - /// Data - /// StackItem - public static StackItem ConvertToStackItem(this object? data) - { - return data switch - { - null => StackItem.Null, - bool b => (VM.Types.Boolean)b, - string s => (ByteString)s, - byte[] d => (ByteString)d, - ReadOnlyMemory r => (ByteString)r, - byte by => (Integer)by, - sbyte sby => (Integer)sby, - short i16 => (Integer)i16, - ushort ui16 => (Integer)ui16, - int i32 => (Integer)i32, - uint ui32 => (Integer)ui32, - long i64 => (Integer)i64, - ulong ui64 => (Integer)ui64, - BigInteger bi => (Integer)bi, - UInt160 u160 => (ByteString)u160.ToArray(), - UInt256 u256 => (ByteString)u256.ToArray(), - Cryptography.ECC.ECPoint ec => (ByteString)ec.ToArray(), - object[] arr => new VM.Types.Array(arr.Select(ConvertToStackItem)), - IEnumerable iarr => new VM.Types.Array(iarr.Select(ConvertToStackItem)), - _ => StackItem.Null, - }; - } - /// /// Convert Array stack item to dotnet array /// @@ -77,28 +46,63 @@ public static StackItem ConvertToStackItem(this object? data) return type switch { - _ when type == typeof(bool) => stackItem.GetBoolean(), _ when type == typeof(string) => Utility.StrictUTF8.GetString(stackItem.GetSpan()), _ when type == typeof(byte[]) => stackItem.GetSpan().ToArray(), + + _ when type == typeof(bool) => stackItem.GetBoolean(), + _ when type == typeof(bool?) => stackItem.GetBoolean(), _ when type == typeof(byte) => (byte)stackItem.GetInteger(), + _ when type == typeof(byte?) => (byte)stackItem.GetInteger(), _ when type == typeof(sbyte) => (sbyte)stackItem.GetInteger(), + _ when type == typeof(sbyte?) => (sbyte)stackItem.GetInteger(), _ when type == typeof(short) => (short)stackItem.GetInteger(), + _ when type == typeof(short?) => (short)stackItem.GetInteger(), _ when type == typeof(ushort) => (ushort)stackItem.GetInteger(), + _ when type == typeof(ushort?) => (ushort)stackItem.GetInteger(), _ when type == typeof(int) => (int)stackItem.GetInteger(), + _ when type == typeof(int?) => (int)stackItem.GetInteger(), _ when type == typeof(uint) => (uint)stackItem.GetInteger(), + _ when type == typeof(uint?) => (uint)stackItem.GetInteger(), _ when type == typeof(long) => (long)stackItem.GetInteger(), + _ when type == typeof(long?) => (long)stackItem.GetInteger(), _ when type == typeof(ulong) => (ulong)stackItem.GetInteger(), + _ when type == typeof(ulong?) => (ulong)stackItem.GetInteger(), + + _ when type.IsEnum => Enum.ToObject(type, (int)stackItem.GetInteger()), _ when type == typeof(BigInteger) => stackItem.GetInteger(), + _ when type == typeof(BigInteger?) => stackItem.GetInteger(), _ when type == typeof(UInt160) => new UInt160(stackItem.GetSpan().ToArray()), _ when type == typeof(UInt256) => new UInt256(stackItem.GetSpan().ToArray()), - _ when type == typeof(Cryptography.ECC.ECPoint) => Cryptography.ECC.ECPoint.FromBytes(stackItem.GetSpan().ToArray(), Cryptography.ECC.ECCurve.Secp256r1), + _ when type == typeof(ECPoint) => ECPoint.FromBytes(stackItem.GetSpan().ToArray(), ECCurve.Secp256r1), _ when type == typeof(List) && stackItem is CompoundType cp => new List(cp.SubItems), // SubItems in StackItem type _ when typeof(IInteroperable).IsAssignableFrom(type) => CreateInteroperable(stackItem, type), + _ when type.IsArray && stackItem is CompoundType cp => CreateTypeArray(cp.SubItems, type.GetElementType()!), + _ when stackItem is InteropInterface it && it.GetInterface().GetType() == type => it.GetInterface(), + _ => throw new FormatException($"Impossible to convert {stackItem} to {type}"), }; } - private static IInteroperable CreateInteroperable(StackItem stackItem, Type type) + private static object CreateTypeArray(IEnumerable objects, Type elementType) + { + var obj = objects.ToArray(); + + if (elementType != typeof(object)) + { + var arr = System.Array.CreateInstance(elementType, obj.Length); + + for (int x = 0; x < arr.Length; x++) + { + arr.SetValue(ConvertTo(obj[x], elementType), x); + } + + return arr; + } + + return obj; + } + + private static object CreateInteroperable(StackItem stackItem, Type type) { var interoperable = (IInteroperable)Activator.CreateInstance(type)!; interoperable.FromStackItem(stackItem); diff --git a/src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt160.cs b/src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt160.cs new file mode 100644 index 000000000..c578928aa --- /dev/null +++ b/src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt160.cs @@ -0,0 +1,15 @@ +namespace Neo.SmartContract.Testing.InvalidTypes +{ + public class InvalidUInt160 + { + /// + /// Null UInt160 + /// + public static readonly UInt160? Null = null; + + /// + /// This will be an invalid UInt160 + /// + public static readonly UInt160 Invalid = new(); + } +} diff --git a/src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt256.cs b/src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt256.cs new file mode 100644 index 000000000..90a63ab8b --- /dev/null +++ b/src/Neo.SmartContract.Testing/InvalidTypes/InvalidUInt256.cs @@ -0,0 +1,15 @@ +namespace Neo.SmartContract.Testing.InvalidTypes +{ + public class InvalidUInt256 + { + /// + /// Null UInt256 + /// + public static readonly UInt256? Null = null; + + /// + /// This will be an invalid UInt256 + /// + public static readonly UInt256 Invalid = new(); + } +} diff --git a/src/Neo.SmartContract.Testing/Native/ContractManagement.cs b/src/Neo.SmartContract.Testing/Native/ContractManagement.cs index b348e26d1..01c73a8be 100644 --- a/src/Neo.SmartContract.Testing/Native/ContractManagement.cs +++ b/src/Neo.SmartContract.Testing/Native/ContractManagement.cs @@ -1,72 +1,100 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; +using Neo.SmartContract.Iterators; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class ContractManagement : Neo.SmartContract.Testing.SmartContract +public abstract class ContractManagement : SmartContract { #region Events + public delegate void delDeploy(UInt160 Hash); + [DisplayName("Deploy")] public event delDeploy? OnDeploy; public delegate void delDestroy(UInt160 Hash); + [DisplayName("Destroy")] public event delDestroy? OnDestroy; public delegate void delUpdate(UInt160 Hash); + [DisplayName("Update")] public event delUpdate? OnUpdate; #endregion + #region Properties - public abstract object ContractHashes { [DisplayName("getContractHashes")] get; } + + /// + /// Safe property + /// + public abstract IIterator ContractHashes { [DisplayName("getContractHashes")] get; } + + /// + /// Safe property + /// public abstract BigInteger MinimumDeploymentFee { [DisplayName("getMinimumDeploymentFee")] get; [DisplayName("setMinimumDeploymentFee")] set; } + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName("getContract")] public abstract ContractState GetContract(UInt160 hash); + /// /// Safe method /// [DisplayName("getContractById")] public abstract ContractState GetContractById(BigInteger id); + /// /// Safe method /// [DisplayName("hasMethod")] public abstract bool HasMethod(UInt160 hash, string method, BigInteger pcount); + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName("deploy")] public abstract ContractState Deploy(byte[] nefFile, byte[] manifest); + /// /// Unsafe method /// [DisplayName("deploy")] public abstract ContractState Deploy(byte[] nefFile, byte[] manifest, object? data = null); + /// /// Unsafe method /// [DisplayName("destroy")] public abstract void Destroy(); + /// /// Unsafe method /// [DisplayName("update")] public abstract void Update(byte[] nefFile, byte[] manifest); + /// /// Unsafe method /// [DisplayName("update")] public abstract void Update(byte[] nefFile, byte[] manifest, object? data = null); + #endregion + #region Constructor for internal use only - protected ContractManagement(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected ContractManagement(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/CryptoLib.cs b/src/Neo.SmartContract.Testing/Native/CryptoLib.cs index a6498111e..54b50f1cd 100644 --- a/src/Neo.SmartContract.Testing/Native/CryptoLib.cs +++ b/src/Neo.SmartContract.Testing/Native/CryptoLib.cs @@ -1,65 +1,77 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class CryptoLib : Neo.SmartContract.Testing.SmartContract +public abstract class CryptoLib : SmartContract { #region Safe methods + /// /// Safe method /// [DisplayName("bls12381Add")] - public abstract object Bls12381Add(object x, object y); + public abstract object? Bls12381Add(object? x, object? y); + /// /// Safe method /// [DisplayName("bls12381Deserialize")] - public abstract object Bls12381Deserialize(byte[] data); + public abstract object? Bls12381Deserialize(byte[]? data); + /// /// Safe method /// [DisplayName("bls12381Equal")] - public abstract bool Bls12381Equal(object x, object y); + public abstract bool? Bls12381Equal(object? x, object? y); + /// /// Safe method /// [DisplayName("bls12381Mul")] - public abstract object Bls12381Mul(object x, byte[] mul, bool neg); + public abstract object? Bls12381Mul(object? x, byte[]? mul, bool? neg); + /// /// Safe method /// [DisplayName("bls12381Pairing")] - public abstract object Bls12381Pairing(object g1, object g2); + public abstract object? Bls12381Pairing(object? g1, object? g2); + /// /// Safe method /// [DisplayName("bls12381Serialize")] - public abstract byte[] Bls12381Serialize(object g); + public abstract byte[]? Bls12381Serialize(object? g); + /// /// Safe method /// [DisplayName("murmur32")] - public abstract byte[] Murmur32(byte[] data, BigInteger seed); + public abstract byte[] Murmur32(byte[]? data, BigInteger? seed); + /// /// Safe method /// [DisplayName("ripemd160")] - public abstract byte[] Ripemd160(byte[] data); + public abstract byte[] Ripemd160(byte[]? data); + /// /// Safe method /// [DisplayName("sha256")] - public abstract byte[] Sha256(byte[] data); + public abstract byte[] Sha256(byte[]? data); + /// /// Safe method /// [DisplayName("verifyWithECDsa")] - public abstract bool VerifyWithECDsa(byte[] message, byte[] pubkey, byte[] signature, BigInteger curve); + public abstract bool VerifyWithECDsa(byte[]? message, byte[]? pubkey, byte[]? signature, BigInteger? curve); + #endregion + #region Constructor for internal use only - protected CryptoLib(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected CryptoLib(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/GasToken.cs b/src/Neo.SmartContract.Testing/Native/GasToken.cs index 9816da823..568aad13d 100644 --- a/src/Neo.SmartContract.Testing/Native/GasToken.cs +++ b/src/Neo.SmartContract.Testing/Native/GasToken.cs @@ -1,37 +1,61 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class GasToken : Neo.SmartContract.Testing.SmartContract +public abstract class GasToken : SmartContract { #region Events - public delegate void delTransfer(UInt160 from, UInt160 to, BigInteger amount); + + public delegate void delTransfer(UInt160? from, UInt160? to, BigInteger? amount); + [DisplayName("Transfer")] public event delTransfer? OnTransfer; + #endregion + #region Properties + + /// + /// Safe property + /// public abstract BigInteger Decimals { [DisplayName("decimals")] get; } + + /// + /// Safe property + /// public abstract string Symbol { [DisplayName("symbol")] get; } + + /// + /// Safe property + /// public abstract BigInteger TotalSupply { [DisplayName("totalSupply")] get; } + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName("balanceOf")] - public abstract BigInteger BalanceOf(UInt160 account); + public abstract BigInteger BalanceOf(UInt160? account); + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName("transfer")] - public abstract bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object? data = null); + public abstract bool Transfer(UInt160? from, UInt160? to, BigInteger? amount, object? data = null); + #endregion + #region Constructor for internal use only - protected GasToken(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected GasToken(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/LedgerContract.cs b/src/Neo.SmartContract.Testing/Native/LedgerContract.cs index ac6c663b1..142a0d852 100644 --- a/src/Neo.SmartContract.Testing/Native/LedgerContract.cs +++ b/src/Neo.SmartContract.Testing/Native/LedgerContract.cs @@ -1,49 +1,70 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Native; +using Neo.VM; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class LedgerContract : Neo.SmartContract.Testing.SmartContract +public abstract class LedgerContract : SmartContract { #region Properties + + /// + /// Safe property + /// public abstract UInt256 CurrentHash { [DisplayName("currentHash")] get; } + + /// + /// Safe property + /// public abstract BigInteger CurrentIndex { [DisplayName("currentIndex")] get; } + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName("getBlock")] - public abstract IList GetBlock(byte[] indexOrHash); + public abstract TrimmedBlock? GetBlock(byte[]? indexOrHash); + /// /// Safe method /// [DisplayName("getTransaction")] - public abstract IList GetTransaction(UInt256 hash); + public abstract Transaction? GetTransaction(UInt256? hash); + /// /// Safe method /// [DisplayName("getTransactionFromBlock")] - public abstract IList GetTransactionFromBlock(byte[] blockIndexOrHash, BigInteger txIndex); + public abstract Transaction? GetTransactionFromBlock(byte[]? blockIndexOrHash, BigInteger? txIndex); + /// /// Safe method /// [DisplayName("getTransactionHeight")] - public abstract BigInteger GetTransactionHeight(UInt256 hash); + public abstract BigInteger? GetTransactionHeight(UInt256? hash); + /// /// Safe method /// [DisplayName("getTransactionSigners")] - public abstract IList GetTransactionSigners(UInt256 hash); + public abstract Signer[]? GetTransactionSigners(UInt256? hash); + /// /// Safe method /// [DisplayName("getTransactionVMState")] - public abstract BigInteger GetTransactionVMState(UInt256 hash); + public abstract VMState GetTransactionVMState(UInt256? hash); + #endregion + #region Constructor for internal use only - protected LedgerContract(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected LedgerContract(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/NeoToken.cs b/src/Neo.SmartContract.Testing/Native/NeoToken.cs index 79e5599e4..82fe6139e 100644 --- a/src/Neo.SmartContract.Testing/Native/NeoToken.cs +++ b/src/Neo.SmartContract.Testing/Native/NeoToken.cs @@ -1,79 +1,171 @@ using Neo.Cryptography.ECC; -using System.Collections.Generic; +using Neo.IO; +using Neo.SmartContract.Iterators; +using Neo.VM; +using Neo.VM.Types; +using System; using System.ComponentModel; using System.Numerics; +using Neo.SmartContract.Testing.Extensions; +using System.Linq; namespace Neo.SmartContract.Testing; -public abstract class NeoToken : Neo.SmartContract.Testing.SmartContract +public abstract class NeoToken : SmartContract { + public class Candidate : IInteroperable + { + /// + /// Public key + /// + public ECPoint? PublicKey { get; private set; } + + /// + /// Votes + /// + public BigInteger Votes { get; private set; } = BigInteger.Zero; + + public void FromStackItem(StackItem stackItem) + { + if (stackItem is not CompoundType cp) throw new FormatException(); + if (cp.Count < 2) throw new FormatException(); + + var items = cp.SubItems.ToArray(); + + PublicKey = (ECPoint)items[0].ConvertTo(typeof(ECPoint))!; + Votes = (BigInteger)items[1].ConvertTo(typeof(BigInteger))!; + } + + public StackItem ToStackItem(ReferenceCounter referenceCounter) + { + return new VM.Types.Array(new StackItem[] { PublicKey.ToArray(), Votes }); + } + } + #region Events + public delegate void delCandidateStateChanged(ECPoint pubkey, bool registered, BigInteger votes); [DisplayName("CandidateStateChanged")] public event delCandidateStateChanged? OnCandidateStateChanged; + public delegate void delTransfer(UInt160 from, UInt160 to, BigInteger amount); [DisplayName("Transfer")] public event delTransfer? OnTransfer; + public delegate void delVote(UInt160 account, ECPoint from, ECPoint to, BigInteger amount); [DisplayName("Vote")] public event delVote? OnVote; + #endregion + #region Properties + + /// + /// Safe property + /// public abstract BigInteger Decimals { [DisplayName("decimals")] get; } - public abstract object AllCandidates { [DisplayName("getAllCandidates")] get; } - public abstract IList Candidates { [DisplayName("getCandidates")] get; } - public abstract IList Committee { [DisplayName("getCommittee")] get; } + + /// + /// Safe property + /// + public abstract IIterator AllCandidates { [DisplayName("getAllCandidates")] get; } + + /// + /// Safe property + /// + public abstract Candidate[] Candidates { [DisplayName("getCandidates")] get; } + + /// + /// Safe property + /// + public abstract ECPoint[] Committee { [DisplayName("getCommittee")] get; } + + /// + /// Safe property + /// public abstract BigInteger GasPerBlock { [DisplayName("getGasPerBlock")] get; [DisplayName("setGasPerBlock")] set; } - public abstract IList NextBlockValidators { [DisplayName("getNextBlockValidators")] get; } + + /// + /// Safe property + /// + public abstract ECPoint[] NextBlockValidators { [DisplayName("getNextBlockValidators")] get; } + + /// + /// Safe property + /// public abstract BigInteger RegisterPrice { [DisplayName("getRegisterPrice")] get; [DisplayName("setRegisterPrice")] set; } + + /// + /// Safe property + /// public abstract string Symbol { [DisplayName("symbol")] get; } + + /// + /// Safe property + /// public abstract BigInteger TotalSupply { [DisplayName("totalSupply")] get; } + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName("balanceOf")] - public abstract BigInteger BalanceOf(UInt160 account); + public abstract BigInteger BalanceOf(UInt160? account); + /// /// Safe method /// [DisplayName("getAccountState")] - public abstract IList GetAccountState(UInt160 account); + public abstract Native.NeoToken.NeoAccountState GetAccountState(UInt160? account); + /// /// Safe method /// [DisplayName("getCandidateVote")] - public abstract BigInteger GetCandidateVote(ECPoint pubKey); + public abstract BigInteger GetCandidateVote(ECPoint? pubKey); + /// /// Safe method /// [DisplayName("unclaimedGas")] - public abstract BigInteger UnclaimedGas(UInt160 account, BigInteger end); + public abstract BigInteger UnclaimedGas(UInt160? account, BigInteger? end); + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName("registerCandidate")] - public abstract bool RegisterCandidate(ECPoint pubkey); + public abstract bool RegisterCandidate(ECPoint? pubkey); + /// /// Unsafe method /// [DisplayName("transfer")] - public abstract bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object? data = null); + public abstract bool Transfer(UInt160? from, UInt160? to, BigInteger? amount, object? data = null); + /// /// Unsafe method /// [DisplayName("unregisterCandidate")] - public abstract bool UnregisterCandidate(ECPoint pubkey); + public abstract bool UnregisterCandidate(ECPoint? pubkey); + /// /// Unsafe method /// [DisplayName("vote")] - public abstract bool Vote(UInt160 account, ECPoint voteTo); + public abstract bool Vote(UInt160? account, ECPoint? voteTo); + #endregion + #region Constructor for internal use only - protected NeoToken(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected NeoToken(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/OracleContract.cs b/src/Neo.SmartContract.Testing/Native/OracleContract.cs index 1964d8dfb..3c83fd3d0 100644 --- a/src/Neo.SmartContract.Testing/Native/OracleContract.cs +++ b/src/Neo.SmartContract.Testing/Native/OracleContract.cs @@ -1,37 +1,55 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class OracleContract : Neo.SmartContract.Testing.SmartContract +public abstract class OracleContract : SmartContract { #region Events + public delegate void delOracleRequest(BigInteger Id, UInt160 RequestContract, string Url, string Filter); [DisplayName("OracleRequest")] public event delOracleRequest? OnOracleRequest; + public delegate void delOracleResponse(BigInteger Id, UInt256 OriginalTx); [DisplayName("OracleResponse")] public event delOracleResponse? OnOracleResponse; + #endregion + #region Properties + + /// + /// Safe property + /// public abstract BigInteger Price { [DisplayName("getPrice")] get; [DisplayName("setPrice")] set; } + + /// + /// Safe property + /// public abstract bool Verify { [DisplayName("verify")] get; } + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName("finish")] public abstract void Finish(); + /// /// Unsafe method /// [DisplayName("request")] - public abstract void Request(string url, string filter, string callback, object userData, BigInteger gasForResponse); + public abstract void Request(string? url, string? filter, string? callback, object? userData, BigInteger? gasForResponse); + #endregion + #region Constructor for internal use only - protected OracleContract(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected OracleContract(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/PolicyContract.cs b/src/Neo.SmartContract.Testing/Native/PolicyContract.cs index 6be01a1b8..fc9737f24 100644 --- a/src/Neo.SmartContract.Testing/Native/PolicyContract.cs +++ b/src/Neo.SmartContract.Testing/Native/PolicyContract.cs @@ -1,47 +1,70 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class PolicyContract : Neo.SmartContract.Testing.SmartContract +public abstract class PolicyContract : SmartContract { #region Properties + + /// + /// Safe property + /// public abstract BigInteger ExecFeeFactor { [DisplayName("getExecFeeFactor")] get; [DisplayName("setExecFeeFactor")] set; } + + /// + /// Safe property + /// public abstract BigInteger FeePerByte { [DisplayName("getFeePerByte")] get; [DisplayName("setFeePerByte")] set; } + + /// + /// Safe property + /// public abstract BigInteger StoragePrice { [DisplayName("getStoragePrice")] get; [DisplayName("setStoragePrice")] set; } + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName("getAttributeFee")] - public abstract BigInteger GetAttributeFee(BigInteger attributeType); + public abstract BigInteger GetAttributeFee(BigInteger? attributeType); + /// /// Safe method /// [DisplayName("isBlocked")] - public abstract bool IsBlocked(UInt160 account); + public abstract bool IsBlocked(UInt160? account); + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName("blockAccount")] - public abstract bool BlockAccount(UInt160 account); + public abstract bool BlockAccount(UInt160? account); + /// /// Unsafe method /// [DisplayName("setAttributeFee")] - public abstract void SetAttributeFee(BigInteger attributeType, BigInteger value); + public abstract void SetAttributeFee(BigInteger? attributeType, BigInteger? value); + /// /// Unsafe method /// [DisplayName("unblockAccount")] - public abstract bool UnblockAccount(UInt160 account); + public abstract bool UnblockAccount(UInt160? account); + #endregion + #region Constructor for internal use only - protected PolicyContract(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected PolicyContract(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/RoleManagement.cs b/src/Neo.SmartContract.Testing/Native/RoleManagement.cs index 1bb7aa07d..62eed6f97 100644 --- a/src/Neo.SmartContract.Testing/Native/RoleManagement.cs +++ b/src/Neo.SmartContract.Testing/Native/RoleManagement.cs @@ -1,32 +1,43 @@ using Neo.Cryptography.ECC; -using System.Collections.Generic; +using Neo.SmartContract.Native; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class RoleManagement : Neo.SmartContract.Testing.SmartContract +public abstract class RoleManagement : SmartContract { #region Events + public delegate void delDesignation(BigInteger Role, BigInteger BlockIndex); [DisplayName("Designation")] public event delDesignation? OnDesignation; + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName("getDesignatedByRole")] - public abstract IList GetDesignatedByRole(BigInteger role, BigInteger index); + public abstract ECPoint[] GetDesignatedByRole(BigInteger? role, BigInteger? index); + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName("designateAsRole")] - public abstract void DesignateAsRole(BigInteger role, IList nodes); + public abstract void DesignateAsRole(Role? role, ECPoint[]? nodes); + #endregion + #region Constructor for internal use only - protected RoleManagement(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected RoleManagement(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/Native/StdLib.cs b/src/Neo.SmartContract.Testing/Native/StdLib.cs index 04fe6f613..b687b9025 100644 --- a/src/Neo.SmartContract.Testing/Native/StdLib.cs +++ b/src/Neo.SmartContract.Testing/Native/StdLib.cs @@ -1,120 +1,143 @@ -using Neo.Cryptography.ECC; -using System.Collections.Generic; using System.ComponentModel; using System.Numerics; namespace Neo.SmartContract.Testing; -public abstract class StdLib : Neo.SmartContract.Testing.SmartContract +public abstract class StdLib : SmartContract { #region Safe methods + /// /// Safe method /// [DisplayName("atoi")] - public abstract BigInteger Atoi(string value); + public abstract BigInteger Atoi(string? value); + /// /// Safe method /// [DisplayName("atoi")] - public abstract BigInteger Atoi(string value, BigInteger @base); + public abstract BigInteger Atoi(string? value, BigInteger? @base); + /// /// Safe method /// [DisplayName("base58CheckDecode")] - public abstract byte[] Base58CheckDecode(string s); + public abstract byte[] Base58CheckDecode(string? s); + /// /// Safe method /// [DisplayName("base58CheckEncode")] - public abstract string Base58CheckEncode(byte[] data); + public abstract string Base58CheckEncode(byte[]? data); + /// /// Safe method /// [DisplayName("base58Decode")] - public abstract byte[] Base58Decode(string s); + public abstract byte[] Base58Decode(string? s); + /// /// Safe method /// [DisplayName("base58Encode")] - public abstract string Base58Encode(byte[] data); + public abstract string Base58Encode(byte[]? data); + /// /// Safe method /// [DisplayName("base64Decode")] - public abstract byte[] Base64Decode(string s); + public abstract byte[] Base64Decode(string? s); + /// /// Safe method /// [DisplayName("base64Encode")] - public abstract string Base64Encode(byte[] data); + public abstract string Base64Encode(byte[]? data); + /// /// Safe method /// [DisplayName("deserialize")] - public abstract object Deserialize(byte[] data); + public abstract object Deserialize(byte[]? data); + /// /// Safe method /// [DisplayName("itoa")] - public abstract string Itoa(BigInteger value); + public abstract string Itoa(BigInteger? value); + /// /// Safe method /// [DisplayName("itoa")] - public abstract string Itoa(BigInteger value, BigInteger @base); + public abstract string Itoa(BigInteger? value, BigInteger? @base); + /// /// Safe method /// [DisplayName("jsonDeserialize")] - public abstract object JsonDeserialize(byte[] json); + public abstract object JsonDeserialize(byte[]? json); + /// /// Safe method /// [DisplayName("jsonSerialize")] public abstract byte[] JsonSerialize(object? item = null); + /// /// Safe method /// [DisplayName("memoryCompare")] - public abstract BigInteger MemoryCompare(byte[] str1, byte[] str2); + public abstract BigInteger MemoryCompare(byte[]? str1, byte[]? str2); + /// /// Safe method /// [DisplayName("memorySearch")] - public abstract BigInteger MemorySearch(byte[] mem, byte[] value); + public abstract BigInteger MemorySearch(byte[]? mem, byte[]? value); + /// /// Safe method /// [DisplayName("memorySearch")] - public abstract BigInteger MemorySearch(byte[] mem, byte[] value, BigInteger start); + public abstract BigInteger MemorySearch(byte[]? mem, byte[]? value, BigInteger? start); + /// /// Safe method /// [DisplayName("memorySearch")] - public abstract BigInteger MemorySearch(byte[] mem, byte[] value, BigInteger start, bool backward); + public abstract BigInteger MemorySearch(byte[]? mem, byte[]? value, BigInteger? start, bool? backward); + /// /// Safe method /// [DisplayName("serialize")] public abstract byte[] Serialize(object? item = null); + /// /// Safe method /// [DisplayName("stringSplit")] - public abstract IList StringSplit(string str, string separator); + public abstract string[] StringSplit(string? str, string? separator); + /// /// Safe method /// [DisplayName("stringSplit")] - public abstract IList StringSplit(string str, string separator, bool removeEmptyEntries); + public abstract string[] StringSplit(string? str, string? separator, bool? removeEmptyEntries); + /// /// Safe method /// [DisplayName("strLen")] - public abstract BigInteger StrLen(string str); + public abstract BigInteger StrLen(string? str); + #endregion + #region Constructor for internal use only - protected StdLib(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + + protected StdLib(SmartContractInitialize initialize) : base(initialize) { } + #endregion } diff --git a/src/Neo.SmartContract.Testing/NativeArtifacts.cs b/src/Neo.SmartContract.Testing/NativeArtifacts.cs index caa7b5f73..7cc88c494 100644 --- a/src/Neo.SmartContract.Testing/NativeArtifacts.cs +++ b/src/Neo.SmartContract.Testing/NativeArtifacts.cs @@ -52,7 +52,7 @@ public class NativeArtifacts public RoleManagement RoleManagement { get; } /// - /// OracleContract + /// StdLib /// public StdLib StdLib { get; } diff --git a/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj b/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj index 15676ae7b..dd3a11f79 100644 --- a/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj +++ b/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj @@ -16,6 +16,7 @@ + diff --git a/src/Neo.SmartContract.Testing/README.md b/src/Neo.SmartContract.Testing/README.md index aab24d859..fd900ba9e 100644 --- a/src/Neo.SmartContract.Testing/README.md +++ b/src/Neo.SmartContract.Testing/README.md @@ -33,7 +33,7 @@ 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 `ABI` 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` 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. 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`. @@ -49,7 +49,7 @@ public class MyUnitTestClass foreach (var n in Native.NativeContract.Contracts) { var manifest = n.Manifest; - var source = manifest.Abi.GetArtifactsSource(manifest.Name); + var source = manifest.GetArtifactsSource(); File.WriteAllText($"{manifest.Name}.cs", source); } @@ -344,4 +344,4 @@ Keep in mind that the coverage is at the instruction level. The currently known limitations are: - Receive events during the deploy, because the object is returned after performing the deploy, it is not possible to intercept notifications for the deploy unless the contract is previously created with `FromHash` knowing the hash of the contract to be created. -- It is possible that if the contract is updated, the coverage calculation may be incorrect. +- It is possible that if the contract is updated, the coverage calculation may be incorrect. The update method of a contract can be tested, but if the same script and abi as the original are not used, it can result in a coverage calculation error. diff --git a/src/Neo.SmartContract.Testing/SmartContract.cs b/src/Neo.SmartContract.Testing/SmartContract.cs index 21fb37847..042173802 100644 --- a/src/Neo.SmartContract.Testing/SmartContract.cs +++ b/src/Neo.SmartContract.Testing/SmartContract.cs @@ -51,7 +51,31 @@ internal StackItem Invoke(string methodName, params object[] args) // Compose script using ScriptBuilder script = new(); - script.EmitDynamicCall(Hash, methodName, args); + + if (args is null || args.Length == 0) + script.Emit(OpCode.NEWARRAY0); + else + { + for (int i = args.Length - 1; i >= 0; i--) + { + var arg = args[i]; + + if (ReferenceEquals(arg, InvalidTypes.InvalidUInt160.Invalid) || + ReferenceEquals(arg, InvalidTypes.InvalidUInt256.Invalid)) + { + arg = System.Array.Empty(); + } + + script.EmitPush(arg); + } + script.EmitPush(args.Length); + script.Emit(OpCode.PACK); + } + + script.EmitPush(CallFlags.All); + script.EmitPush(methodName); + script.EmitPush(Hash); + script.EmitSysCall(ApplicationEngine.System_Contract_Call); // Execute @@ -79,7 +103,9 @@ internal void InvokeOnNotify(string eventName, VM.Types.Array state) var ev = _contractType.GetEvent(eventName); if (ev is null) { - ev = _contractType.GetEvents().FirstOrDefault(u => u.GetCustomAttribute()?.DisplayName == eventName); + ev = _contractType.GetEvents() + .FirstOrDefault(u => u.Name == eventName || u.GetCustomAttribute(true)?.DisplayName == eventName); + if (ev is null) { _notifyCache[eventName] = null; diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index e6a5ec171..0d0fe4a58 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -233,6 +233,18 @@ internal void ApplicationEngineLog(object? sender, LogEventArgs e) #endregion + /// + /// Get deploy hash + /// + /// Nef + /// Manifest + /// Contract hash + public UInt160 GetDeployHash(byte[] nef, string manifest) + { + return Helper.GetContractHash(Sender, + nef.AsSerializable().CheckSum, ContractManifest.Parse(manifest).Name); + } + /// /// Deploy Smart contract /// @@ -265,7 +277,19 @@ public T Deploy(NefFile nef, ContractManifest manifest, object? data = null, // Mock contract //UInt160 hash = Helper.GetContractHash(Sender, nef.CheckSum, manifest.Name); - return MockContract(state.Hash, state.Id, customMock); + var ret = MockContract(state.Hash, state.Id, customMock); + + // We cache the coverage contract during `_deploy` + // at this moment we don't have the abi stored + // so we need to regenerate the coverage methods + + if (EnableCoverageCapture) + { + var coverage = GetCoverage(ret); + coverage?.GenerateMethods(state.Manifest.Abi, state.Script); + } + + return ret; } /// @@ -457,7 +481,11 @@ public StackItem Execute(Script script) { if (!Coverage.TryGetValue(contract.Hash, out var coveredContract)) { - return null; + var state = Neo.SmartContract.Native.NativeContract.ContractManagement.GetContract(Storage.Snapshot, contract.Hash); + if (state == null) return null; + + coveredContract = new(contract.Hash, state.Manifest.Abi, state.Script); + Coverage[coveredContract.Hash] = coveredContract; } return coveredContract; @@ -471,9 +499,7 @@ public StackItem Execute(Script script) /// CoveredContract public CoverageBase? GetCoverage(T contract, string methodName, int pcount) where T : SmartContract { - var coveredContract = GetCoverage(contract); - - return coveredContract?.GetCoverage(methodName, pcount); + return GetCoverage(contract)?.GetCoverage(methodName, pcount); } /// @@ -485,10 +511,8 @@ public StackItem Execute(Script script) /// CoveredContract public CoverageBase? GetCoverage(T contract, Expression> method) where T : SmartContract { - if (!Coverage.TryGetValue(contract.Hash, out var coveredContract)) - { - return null; - } + var coveredContract = GetCoverage(contract); + if (coveredContract == null) return null; var abiMethods = AbiMethod.CreateFromExpression(method.Body) .Select(coveredContract.GetCoverage) @@ -514,10 +538,8 @@ public StackItem Execute(Script script) /// CoveredContract public CoverageBase? GetCoverage(T contract, Expression> method) where T : SmartContract { - if (!Coverage.TryGetValue(contract.Hash, out var coveredContract)) - { - return null; - } + var coveredContract = GetCoverage(contract); + if (coveredContract == null) return null; var abiMethods = AbiMethod.CreateFromExpression(method.Body) .Select(coveredContract.GetCoverage) diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 72b1df211..3ce77e5d5 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -1,6 +1,5 @@ using Neo.Network.P2P.Payloads; using Neo.Persistence; -using Neo.SmartContract.Testing.Coverage; using Neo.SmartContract.Testing.Extensions; using Neo.VM; using System; @@ -66,14 +65,7 @@ protected override void PostExecuteInstruction(Instruction instruction) if (InstructionPointer is null) return; - if (!coveredContract.CoverageData.TryGetValue(InstructionPointer.Value, out var coverage)) - { - // Note: This call is unusual, out of the expected - - coveredContract.CoverageData[InstructionPointer.Value] = coverage = new CoverageHit(InstructionPointer.Value, true); - } - - coverage.Hit(GasConsumed - PreExecuteInstructionGasConsumed); + coveredContract.Hit(InstructionPointer.Value, GasConsumed - PreExecuteInstructionGasConsumed); } protected override void OnSysCall(InteropDescriptor descriptor) diff --git a/src/Neo.SmartContract.Testing/TestingStandards/INep17Standard.cs b/src/Neo.SmartContract.Testing/TestingStandards/INep17Standard.cs new file mode 100644 index 000000000..d17e6d96f --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingStandards/INep17Standard.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using System.Numerics; + +namespace Neo.SmartContract.Testing.TestingStandards; + +public interface INep17Standard +{ + #region Events + + public delegate void delTransfer(UInt160? from, UInt160? to, BigInteger? amount); + + [DisplayName("Transfer")] + public event delTransfer? OnTransfer; + + #endregion + + #region Properties + + /// + /// Safe method + /// + public string? Symbol { [DisplayName("symbol")] get; } + + /// + /// Safe method + /// + public BigInteger? Decimals { [DisplayName("decimals")] get; } + + /// + /// Safe method + /// + public BigInteger? TotalSupply { [DisplayName("totalSupply")] get; } + + #endregion + + #region Safe methods + + /// + /// Safe method + /// + [DisplayName("balanceOf")] + public BigInteger? BalanceOf(UInt160? owner); + + #endregion + + #region Unsafe methods + + /// + /// Unsafe method + /// + [DisplayName("transfer")] + public bool? Transfer(UInt160? from, UInt160? to, BigInteger? amount, object? data = null); + + #endregion +} diff --git a/src/Neo.SmartContract.Testing/TestingStandards/IOwnable.cs b/src/Neo.SmartContract.Testing/TestingStandards/IOwnable.cs new file mode 100644 index 000000000..6d8500a21 --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingStandards/IOwnable.cs @@ -0,0 +1,24 @@ +using System.ComponentModel; + +namespace Neo.SmartContract.Testing.TestingStandards; + +public interface IOwnable +{ + #region Events + + public delegate void delSetOwner(UInt160? previousOwner, UInt160? newOwner); + + [DisplayName("SetOwner")] + public event delSetOwner? OnSetOwner; + + #endregion + + #region Properties + + /// + /// Safe property + /// + public UInt160? Owner { [DisplayName("getOwner")] get; [DisplayName("setOwner")] set; } + + #endregion +} diff --git a/src/Neo.SmartContract.Testing/TestingStandards/IVerificable.cs b/src/Neo.SmartContract.Testing/TestingStandards/IVerificable.cs new file mode 100644 index 000000000..ec0a3b8f6 --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingStandards/IVerificable.cs @@ -0,0 +1,11 @@ +using System.ComponentModel; + +namespace Neo.SmartContract.Testing.TestingStandards; + +public interface IVerificable +{ + /// + /// Safe property + /// + public bool? Verify { [DisplayName("verify")] get; } +} diff --git a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs new file mode 100644 index 000000000..3b0fa94e6 --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs @@ -0,0 +1,156 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.Testing.InvalidTypes; +using Neo.VM; +using System.Collections.Generic; +using System.Numerics; + +namespace Neo.SmartContract.Testing.TestingStandards; + +public class Nep17Tests : TestBase + where T : SmartContract, INep17Standard +{ + /// + /// Expected total supply + /// + public virtual BigInteger ExpectedTotalSupply => 0; + + /// + /// Expected Decimals + /// + public virtual byte ExpectedDecimals => 8; + + /// + /// Expected symbol + /// + public virtual string ExpectedSymbol => "EXAMPLE"; + + #region Transfer event checks + + private List<(UInt160? from, UInt160? to, BigInteger? amount)> raisedTransfer = new(); + + #endregion + + /// + /// Initialize Test + /// + public Nep17Tests(string nefFile, string manifestFile) : base(nefFile, manifestFile) + { + Contract.OnTransfer += onTransfer; + } + + void onTransfer(UInt160? from, UInt160? to, BigInteger? amount) + { + raisedTransfer.Add((from, to, amount)); + } + + #region Asserts + + /// + /// Assert that Transfer event was raised + /// + /// From + /// To + /// Amount + public void AssertTransferEvent(UInt160? from, UInt160? to, BigInteger? amount) + { + Assert.AreEqual(1, raisedTransfer.Count); + Assert.AreEqual(raisedTransfer[0].from, from); + Assert.AreEqual(raisedTransfer[0].to, to); + Assert.AreEqual(raisedTransfer[0].amount, amount); + raisedTransfer.Clear(); + } + + /// + /// Assert that Transfer event was NOT raised + /// + public void AssertNoTransferEvent() + { + Assert.AreEqual(0, raisedTransfer.Count); + } + + #endregion + + #region Tests + + [TestMethod] + public virtual void TestDecimals() + { + Assert.AreEqual(ExpectedDecimals, Contract.Decimals); + } + + [TestMethod] + public virtual void TestSymbol() + { + Assert.AreEqual(ExpectedSymbol, Contract.Symbol); + } + + [TestMethod] + public virtual void TestTotalSupply() + { + Assert.AreEqual(ExpectedTotalSupply, Contract.TotalSupply); + } + + [TestMethod] + public virtual void TestBalanceOf() + { + Assert.AreEqual(0, Contract.BalanceOf(Bob.Account)); + Assert.ThrowsException(() => Contract.BalanceOf(InvalidUInt160.Null)); + Assert.ThrowsException(() => Contract.BalanceOf(InvalidUInt160.Invalid)); + } + + [TestMethod] + public virtual void TestTransfer() + { + // Invoke transfer from Alice to Bob + + Engine.SetTransactionSigners(Alice); + + var initialSupply = Contract.TotalSupply; + var fromBalance = Contract.BalanceOf(Alice.Account); + + Assert.IsTrue(fromBalance > 5, "Alice needs at least 5 tokens"); + Assert.IsTrue(Contract.Transfer(Alice.Account, Bob.Account, 3)); + + Assert.AreEqual(fromBalance - 3, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(3, Contract.BalanceOf(Bob.Account)); + Assert.AreEqual(initialSupply, Contract.TotalSupply); + AssertTransferEvent(Alice.Account, Bob.Account, 3); + + // Invoke invalid transfers + + Assert.ThrowsException(() => Assert.IsTrue(Contract.Transfer(Alice.Account, Bob.Account, -1))); + Assert.ThrowsException(() => Assert.IsTrue(Contract.Transfer(InvalidUInt160.Null, Bob.Account, -1))); + Assert.ThrowsException(() => Assert.IsTrue(Contract.Transfer(Alice.Account, InvalidUInt160.Null, 0))); + + Assert.ThrowsException(() => Assert.IsTrue(Contract.Transfer(Alice.Account, Bob.Account, -1))); + Assert.ThrowsException(() => Assert.IsTrue(Contract.Transfer(InvalidUInt160.Invalid, Bob.Account, -1))); + Assert.ThrowsException(() => Assert.IsTrue(Contract.Transfer(Alice.Account, InvalidUInt160.Invalid, 0))); + + // Invoke transfer without signature + + Engine.SetTransactionSigners(Bob); + Assert.IsFalse(Contract.Transfer(Alice.Account, Bob.Account, 1)); + AssertNoTransferEvent(); + + // Check with more balance + + Assert.IsFalse(Contract.Transfer(Bob.Account, Alice.Account, 4)); + AssertNoTransferEvent(); + + // Check with not signed + + Assert.IsFalse(Contract.Transfer(Alice.Account, Bob.Account, 0)); + AssertNoTransferEvent(); + + // Return the balance to Allice + + Assert.IsTrue(Contract.Transfer(Bob.Account, Alice.Account, 3)); + + Assert.AreEqual(fromBalance, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(0, Contract.BalanceOf(Bob.Account)); + Assert.AreEqual(initialSupply, Contract.TotalSupply); + AssertTransferEvent(Bob.Account, Alice.Account, 3); + } + + #endregion +} diff --git a/src/Neo.SmartContract.Testing/TestingStandards/OwnableTests.cs b/src/Neo.SmartContract.Testing/TestingStandards/OwnableTests.cs new file mode 100644 index 000000000..3ab6cf3bb --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingStandards/OwnableTests.cs @@ -0,0 +1,114 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.Testing.InvalidTypes; +using Neo.VM; +using System; +using System.Collections.Generic; + +namespace Neo.SmartContract.Testing.TestingStandards; + +public class OwnableTests : TestBase + where T : SmartContract, IOwnable +{ + #region Transfer event checks + + private List<(UInt160? from, UInt160? to)> raisedOnChangeOwner = new(); + + #endregion + + /// + /// Initialize Test + /// + public OwnableTests(string nefFile, string manifestFile) : base(nefFile, manifestFile) + { + Contract.OnSetOwner += onSetOwner; + } + + void onSetOwner(UInt160? from, UInt160? to) + { + raisedOnChangeOwner.Add((from, to)); + } + + #region Asserts + + /// + /// Assert that OnChangeOwner event was raised + /// + /// From + /// To + public void AssertOnChangeOwnerEvent(UInt160? from, UInt160? to) + { + Assert.AreEqual(1, raisedOnChangeOwner.Count); + Assert.AreEqual(raisedOnChangeOwner[0].from, from); + Assert.AreEqual(raisedOnChangeOwner[0].to, to); + raisedOnChangeOwner.Clear(); + } + + /// + /// Assert that Transfer event was NOT raised + /// + public void AssertNoOnChangeOwnerEvent() + { + Assert.AreEqual(0, raisedOnChangeOwner.Count); + } + + #endregion + + #region Tests + + [TestMethod] + public virtual void TestVerify() + { + if (Contract is IVerificable verificable) + { + Engine.SetTransactionSigners(Alice); + Assert.IsTrue(verificable.Verify); + Engine.SetTransactionSigners(TestEngine.GetNewSigner()); + Assert.IsFalse(verificable.Verify); + } + } + + [TestMethod] + public virtual void TestSenderAsDefaultOwner() + { + var random = TestEngine.GetNewSigner(); + + Engine.SetTransactionSigners(random); + + var expectedHash = Engine.GetDeployHash(NefFile, Manifest); + var check = Engine.FromHash(expectedHash, false); + + check.OnSetOwner += onSetOwner; + var ownable = Engine.Deploy(NefFile, Manifest, null); + Assert.AreEqual(check.Hash, ownable.Hash); + check.OnSetOwner -= onSetOwner; + + AssertOnChangeOwnerEvent(null, random.Account); + Assert.AreEqual(random.Account, ownable.Owner); + } + + [TestMethod] + public virtual void TestSetGetOwner() + { + // Alice is the deployer + + Assert.AreEqual(Alice.Account, Contract.Owner); + Engine.SetTransactionSigners(Bob); + Assert.ThrowsException(() => Contract.Owner = Bob.Account); + + Engine.SetTransactionSigners(Alice); + Assert.ThrowsException(() => Contract.Owner = UInt160.Zero); + Assert.ThrowsException(() => Contract.Owner = InvalidUInt160.Null); + Assert.ThrowsException(() => Contract.Owner = InvalidUInt160.Invalid); + + Contract.Owner = Bob.Account; + Assert.AreEqual(Bob.Account, Contract.Owner); + Assert.ThrowsException(() => Contract.Owner = Bob.Account); + + Engine.SetTransactionSigners(Bob); + + Contract.Owner = Alice.Account; + Assert.AreEqual(Alice.Account, Contract.Owner); + } + + #endregion +} diff --git a/src/Neo.SmartContract.Testing/TestingStandards/TestBase.cs b/src/Neo.SmartContract.Testing/TestingStandards/TestBase.cs new file mode 100644 index 000000000..92c39fd0e --- /dev/null +++ b/src/Neo.SmartContract.Testing/TestingStandards/TestBase.cs @@ -0,0 +1,46 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Network.P2P.Payloads; +using Neo.SmartContract.Testing.Coverage; +using System.IO; + +namespace Neo.SmartContract.Testing.TestingStandards; + +public class TestBase where T : SmartContract +{ + public static CoveredContract? Coverage { get; private set; } + public static Signer Alice { get; } = TestEngine.GetNewSigner(); + public static Signer Bob { get; } = TestEngine.GetNewSigner(); + + public byte[] NefFile { get; } + public string Manifest { get; } + public TestEngine Engine { get; } + public T Contract { get; } + public UInt160 ContractHash => Contract.Hash; + + /// + /// Initialize Test + /// + public TestBase(string nefFile, string manifestFile) + { + NefFile = File.ReadAllBytes(nefFile); + Manifest = File.ReadAllText(manifestFile); + + Engine = new TestEngine(true); + Engine.SetTransactionSigners(Alice); + Contract = Engine.Deploy(NefFile, Manifest, null); + + if (Coverage is null) + { + Coverage = Contract.GetCoverage()!; + Assert.IsNotNull(Coverage); + } + } + + [TestCleanup] + public virtual void OnCleanup() + { + // Join the current coverage into the static one + + Coverage?.Join(Contract.GetCoverage()); + } +} diff --git a/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj b/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj new file mode 100644 index 000000000..252976257 --- /dev/null +++ b/tests/Neo.SmartContract.Template.UnitTests/Neo.SmartContract.Template.UnitTests.csproj @@ -0,0 +1,34 @@ + + + + net7.0 + enable + latest + enable + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs new file mode 100644 index 000000000..f242a2a8b --- /dev/null +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs @@ -0,0 +1,27 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Neo.SmartContract.Template.UnitTests.templates.neocontractnep17 +{ + [TestClass] + public class CoverageContractTests + { + /// + /// Required coverage to be success + /// + public static float RequiredCoverage { get; set; } = 0.95F; + + [AssemblyCleanup] + public static void EnsureCoverage() + { + // Ennsure that the coverage is more than X% at the end of the tests + + var coverage = Nep17ContractTests.Coverage; + coverage?.Join(OwnerContractTests.Coverage); + + Assert.IsNotNull(coverage); + + Console.WriteLine(coverage.Dump()); + 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 new file mode 100644 index 000000000..559e1b206 --- /dev/null +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/Nep17ContractTests.cs @@ -0,0 +1,190 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.Testing; +using Neo.SmartContract.Testing.TestingStandards; +using Neo.VM; +using System.Numerics; + +namespace Neo.SmartContract.Template.UnitTests.templates.neocontractnep17 +{ + /// + /// You need to build the solution to resolve Nep17Contract class. + /// + [TestClass] + public class Nep17ContractTests : Nep17Tests + { + #region Expected values in base tests + + public override BigInteger ExpectedTotalSupply => 0; + public override string ExpectedSymbol => "EXAMPLE"; + public override byte ExpectedDecimals => 8; + + #endregion + + /// + /// Initialize Test + /// + public Nep17ContractTests() : + base( + "templates/neocontractnep17/Artifacts/Nep17Contract.nef", + "templates/neocontractnep17/Artifacts/Nep17Contract.manifest.json" + ) + { } + + [TestMethod] + public void TestMyMethod() + { + Assert.AreEqual("World", Contract.MyMethod()); + } + + [TestMethod] + public override void TestTransfer() + { + Engine.SetTransactionSigners(Alice); + + // Test mint + + Assert.AreEqual(0, Contract.TotalSupply); + + // Alice is the owner + + Engine.SetTransactionSigners(Alice); + + Contract.Mint(Alice.Account, 10); + + Assert.AreEqual(10, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(10, Contract.TotalSupply); + AssertTransferEvent(null, Alice.Account, 10); + + // Transfer is done between alice balance to bob + + base.TestTransfer(); + + // Test Burn + + Engine.SetTransactionSigners(Alice); + + Contract.Burn(Alice.Account, Contract.BalanceOf(Alice.Account)); + Contract.Burn(Bob.Account, Contract.BalanceOf(Bob.Account)); + + Assert.AreEqual(0, Contract.TotalSupply); + } + + [TestMethod] + public void TestMintAndBurn() + { + // Alice is the owner + + Engine.SetTransactionSigners(Alice); + + // Test mint -1 + + Assert.ThrowsException(() => Contract.Mint(Alice.Account, -1)); + + // Test mint 0 + + Contract.Mint(Alice.Account, 0); + + Assert.AreEqual(0, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(0, Contract.TotalSupply); + AssertNoTransferEvent(); + + // test mint + + Contract.Mint(Alice.Account, 10); + + Assert.AreEqual(10, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(10, Contract.TotalSupply); + AssertTransferEvent(null, Alice.Account, 10); + + // Test burn -1 + + Assert.ThrowsException(() => Contract.Burn(Alice.Account, -1)); + + // Test burn 0 + + Contract.Burn(Alice.Account, 0); + + Assert.AreEqual(10, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(10, Contract.TotalSupply); + AssertNoTransferEvent(); + + // Test burn + + Contract.Burn(Alice.Account, 10); + + Assert.AreEqual(0, Contract.BalanceOf(Alice.Account)); + Assert.AreEqual(0, Contract.TotalSupply); + AssertTransferEvent(Alice.Account, null, 10); + + // Can't burn more than the BalanceOf + + Assert.ThrowsException(() => Contract.Burn(Alice.Account, 1)); + Assert.ThrowsException(() => Contract.Burn(Bob.Account, 1)); + + // Now check with Bob + + Engine.SetTransactionSigners(Bob); + Assert.ThrowsException(() => Contract.Mint(Alice.Account, 10)); + Assert.ThrowsException(() => Contract.Burn(Alice.Account, 10)); + + // Clean + + Assert.AreEqual(0, Contract.TotalSupply); + } + + [TestMethod] + public void TestUpdate() + { + // Alice is the deployer + + Engine.SetTransactionSigners(Bob); + + Assert.ThrowsException(() => Contract.Update(NefFile, Manifest)); + + Engine.SetTransactionSigners(Alice); + + // Test Update with the same script + + Contract.Update(NefFile, Manifest); + + // Ensure that it works with the same script + + TestTotalSupply(); + } + + [TestMethod] + public void TestDeployWithOwner() + { + // Alice is the deployer + + Engine.SetTransactionSigners(Bob); + + // Test SetOwner notification + + UInt160? previousOwnerRaised = null; + UInt160? newOwnerRaised = null; + + var expectedHash = Engine.GetDeployHash(NefFile, Manifest); + var check = Engine.FromHash(expectedHash, false); + check.OnSetOwner += (previous, newOwner) => + { + previousOwnerRaised = previous; + newOwnerRaised = newOwner; + }; + + // Deploy with random owner, we can use the same storage + // because the contract hash contains the Sender, and now it's random + + var rand = TestEngine.GetNewSigner().Account; + var nep17 = Engine.Deploy(NefFile, Manifest, rand); + Assert.AreEqual(check.Hash, nep17.Hash); + + Coverage?.Join(nep17.GetCoverage()); + + Assert.AreEqual(rand, nep17.Owner); + Assert.IsNull(previousOwnerRaised); + Assert.AreEqual(newOwnerRaised, nep17.Owner); + Assert.AreEqual(newOwnerRaised, rand); + } + } +} diff --git a/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs new file mode 100644 index 000000000..f69001b95 --- /dev/null +++ b/tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/OwnerContractTests.cs @@ -0,0 +1,23 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.Testing; +using Neo.SmartContract.Testing.TestingStandards; + +namespace Neo.SmartContract.Template.UnitTests.templates.neocontractnep17 +{ + /// + /// You need to build the solution to resolve Nep17Contract class. + /// + [TestClass] + public class OwnerContractTests : OwnableTests + { + /// + /// Initialize Test + /// + public OwnerContractTests() : + base( + "templates/neocontractnep17/Artifacts/Nep17Contract.nef", + "templates/neocontractnep17/Artifacts/Nep17Contract.manifest.json" + ) + { } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs index 1c1ca6ca2..4f6296337 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Coverage/CoverageDataTests.cs @@ -18,28 +18,30 @@ public void TestDump() Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); Assert.AreEqual(@" -| 0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5 | 5.26% | -| balanceOf,1 | 0.00% | -| decimals,0 | 0.00% | -| getAccountState,1 | 0.00% | -| getAllCandidates,0 | 0.00% | -| getCandidateVote,1 | 0.00% | -| getCandidates,0 | 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% | -| totalSupply,0 | 100.00% | -| transfer,4 | 0.00% | -| unclaimedGas,2 | 0.00% | -| unregisterCandidate,1 | 0.00% | -| vote,2 | 0.00% | - - +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% │ +└-────────────────────────-┴-───────-┘ ".Trim(), engine.GetCoverage(engine.Native.NEO)?.Dump().Trim()); } @@ -59,7 +61,7 @@ public void TestCoverageByEngine() // Check totalSupply - Assert.IsNull(engine.GetCoverage(engine.Native.NEO)); + Assert.IsNotNull(engine.GetCoverage(engine.Native.NEO)); Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); Assert.AreEqual(engine.Native.NEO.Hash, engine.GetCoverage(engine.Native.NEO)?.Hash); @@ -78,7 +80,7 @@ public void TestCoverageByEngine() // Check coverage by method and expression var methodCovered = engine.GetCoverage(engine.Native.Oracle, o => o.Finish()); - Assert.IsNull(methodCovered); + Assert.IsNotNull(methodCovered); methodCovered = engine.GetCoverage(engine.Native.NEO, o => o.TotalSupply); Assert.AreEqual(3, methodCovered?.TotalInstructions); @@ -99,7 +101,7 @@ public void TestCoverageByEngine() // Check coverage by raw method methodCovered = engine.GetCoverage(engine.Native.Oracle, "finish", 0); - Assert.IsNull(methodCovered); + Assert.IsNotNull(methodCovered); methodCovered = engine.GetCoverage(engine.Native.NEO, "totalSupply", 0); Assert.AreEqual(3, methodCovered?.TotalInstructions); @@ -130,7 +132,7 @@ public void TestCoverageByExtension() // Check totalSupply - Assert.IsNull(engine.Native.NEO.GetCoverage()); + Assert.IsNotNull(engine.Native.NEO.GetCoverage()); Assert.AreEqual(100_000_000, engine.Native.NEO.TotalSupply); Assert.AreEqual(engine.Native.NEO.Hash, engine.Native.NEO.GetCoverage()?.Hash); @@ -149,7 +151,7 @@ public void TestCoverageByExtension() // Check coverage by method and expression var methodCovered = engine.Native.Oracle.GetCoverage(o => o.Finish()); - Assert.IsNull(methodCovered); + Assert.IsNotNull(methodCovered); methodCovered = engine.Native.NEO.GetCoverage(o => o.TotalSupply); Assert.AreEqual(3, methodCovered?.TotalInstructions); @@ -166,7 +168,7 @@ public void TestCoverageByExtension() // Check coverage by raw method methodCovered = engine.GetCoverage(engine.Native.Oracle, "finish", 0); - Assert.IsNull(methodCovered); + Assert.IsNotNull(methodCovered); methodCovered = engine.GetCoverage(engine.Native.NEO, "totalSupply", 0); Assert.AreEqual(3, methodCovered?.TotalInstructions); diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs index 66fa2633c..b3c0cfef1 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/ArtifactExtensionsTests.cs @@ -16,7 +16,7 @@ public void TestGetArtifactsSource() // Create artifacts - var source = manifest.Abi.GetArtifactsSource(manifest.Name, generateProperties: true); + var source = manifest.GetArtifactsSource(manifest.Name, generateProperties: true); Assert.AreEqual(source, @" using Neo.Cryptography.ECC; @@ -26,69 +26,109 @@ public void TestGetArtifactsSource() namespace Neo.SmartContract.Testing; -public abstract class Contract1 : Neo.SmartContract.Testing.SmartContract +public abstract class Contract1 : Neo.SmartContract.Testing.SmartContract, Neo.SmartContract.Testing.TestingStandards.INep17Standard, Neo.SmartContract.Testing.TestingStandards.IVerificable { #region Events - public delegate void delSetOwner(UInt160 newOwner); + + public delegate void delSetOwner(UInt160? newOwner); + [DisplayName(""SetOwner"")] public event delSetOwner? OnSetOwner; - public delegate void delTransfer(UInt160 from, UInt160 to, BigInteger amount); + [DisplayName(""Transfer"")] - public event delTransfer? OnTransfer; + public event Neo.SmartContract.Testing.TestingStandards.INep17Standard.delTransfer? OnTransfer; + #endregion + #region Properties - public abstract BigInteger Decimals { [DisplayName(""decimals"")] get; } - public abstract UInt160 Owner { [DisplayName(""getOwner"")] get; [DisplayName(""setOwner"")] set; } - public abstract string Symbol { [DisplayName(""symbol"")] get; } - public abstract BigInteger TotalSupply { [DisplayName(""totalSupply"")] get; } - public abstract bool Verify { [DisplayName(""verify"")] get; } + + /// + /// Safe property + /// + public abstract BigInteger? Decimals { [DisplayName(""decimals"")] get; } + + /// + /// Safe property + /// + public abstract UInt160? Owner { [DisplayName(""getOwner"")] get; [DisplayName(""setOwner"")] set; } + + /// + /// Safe property + /// + public abstract string? Symbol { [DisplayName(""symbol"")] get; } + + /// + /// Safe property + /// + public abstract BigInteger? TotalSupply { [DisplayName(""totalSupply"")] get; } + + /// + /// Safe property + /// + public abstract bool? Verify { [DisplayName(""verify"")] get; } + #endregion + #region Safe methods + /// /// Safe method /// [DisplayName(""balanceOf"")] - public abstract BigInteger BalanceOf(UInt160 owner); + public abstract BigInteger? BalanceOf(UInt160? owner); + #endregion + #region Unsafe methods + /// /// Unsafe method /// [DisplayName(""burn"")] - public abstract void Burn(UInt160 account, BigInteger amount); + public abstract void Burn(UInt160? account, BigInteger? amount); + /// /// Unsafe method /// [DisplayName(""mint"")] - public abstract void Mint(UInt160 to, BigInteger amount); + public abstract void Mint(UInt160? to, BigInteger? amount); + /// /// Unsafe method /// [DisplayName(""myMethod"")] - public abstract string MyMethod(); + public abstract string? MyMethod(); + /// /// Unsafe method /// [DisplayName(""onNEP17Payment"")] - public abstract void OnNEP17Payment(UInt160 from, BigInteger amount, object? data = null); + public abstract void OnNEP17Payment(UInt160? from, BigInteger? amount, object? data = null); + /// /// Unsafe method /// [DisplayName(""transfer"")] - public abstract bool Transfer(UInt160 from, UInt160 to, BigInteger amount, object? data = null); + public abstract bool? Transfer(UInt160? from, UInt160? to, BigInteger? amount, object? data = null); + /// /// Unsafe method /// [DisplayName(""update"")] - public abstract void Update(byte[] nefFile, string manifest); + public abstract void Update(byte[]? nefFile, string? manifest); + /// /// Unsafe method /// [DisplayName(""withdraw"")] - public abstract bool Withdraw(UInt160 token, UInt160 to, BigInteger amount); + public abstract bool? Withdraw(UInt160? token, UInt160? to, BigInteger? amount); + #endregion + #region Constructor for internal use only + protected Contract1(Neo.SmartContract.Testing.SmartContractInitialize initialize) : base(initialize) { } + #endregion } ".Replace("\r\n", "\n").TrimStart()); diff --git a/tests/Neo.SmartContract.Testing.UnitTests/Extensions/TestExtensionsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/TestExtensionsTests.cs new file mode 100644 index 000000000..b909d3f38 --- /dev/null +++ b/tests/Neo.SmartContract.Testing.UnitTests/Extensions/TestExtensionsTests.cs @@ -0,0 +1,19 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.SmartContract.Testing.Extensions; +using Neo.VM; +using Neo.VM.Types; + +namespace Neo.SmartContract.TestEngine.UnitTests.Extensions +{ + [TestClass] + public class TestExtensionsTests + { + [TestMethod] + public void TestConvertEnum() + { + StackItem stackItem = new Integer((int)VMState.FAULT); + + Assert.AreEqual(VMState.FAULT, (VMState)stackItem.ConvertTo(typeof(VMState))); + } + } +} diff --git a/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs index 949391761..ce45fa1bb 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/NativeArtifactsTests.cs @@ -34,9 +34,9 @@ public void TestInitialize() // Check coverage - Assert.AreEqual(100.0F, engine.Native.NEO.GetCoverage(o => o.Symbol).CoveredPercentage); - Assert.AreEqual(100.0F, engine.Native.NEO.GetCoverage(o => o.TotalSupply).CoveredPercentage); - Assert.AreEqual(100.0F, engine.Native.NEO.GetCoverage(o => o.BalanceOf(It.IsAny())).CoveredPercentage); + Assert.AreEqual(1F, engine.Native.NEO.GetCoverage(o => o.Symbol).CoveredPercentage); + Assert.AreEqual(1F, engine.Native.NEO.GetCoverage(o => o.TotalSupply).CoveredPercentage); + Assert.AreEqual(1F, engine.Native.NEO.GetCoverage(o => o.BalanceOf(It.IsAny())).CoveredPercentage); } [TestMethod] diff --git a/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs b/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs index dbe763fcf..728f4ab0d 100644 --- a/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs +++ b/tests/Neo.SmartContract.Testing.UnitTests/TestEngineTests.cs @@ -22,7 +22,7 @@ public void GenerateNativeArtifacts() foreach (var n in Native.NativeContract.Contracts) { var manifest = n.Manifest; - var source = manifest.Abi.GetArtifactsSource(manifest.Name, generateProperties: true); + var source = manifest.GetArtifactsSource(manifest.Name, generateProperties: true); var fullPath = Path.GetFullPath($"../../../../../src/Neo.SmartContract.Testing/Native/{manifest.Name}.cs"); File.WriteAllText(fullPath, source); From 63320ab2e63efe8c9554c4179e33320bab2928e7 Mon Sep 17 00:00:00 2001 From: Shargon Date: Sun, 18 Feb 2024 22:51:48 +0100 Subject: [PATCH 05/22] Update README.md (#909) --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 54d5e5435..61d14c36f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@

- neo-logo

From 92a94fcd157f4af87fc572187de95bf4cc569b0e Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 19 Feb 2024 09:31:50 +0100 Subject: [PATCH 06/22] Fix workflow issues (#910) * Update pkgs-delete.yml * Move release --- .github/{workflows => }/release.yml | 0 .github/workflows/pkgs-delete.yml | 2 ++ 2 files changed, 2 insertions(+) rename .github/{workflows => }/release.yml (100%) diff --git a/.github/workflows/release.yml b/.github/release.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/release.yml diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index 8839eb1d7..fee4091b8 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -16,6 +16,7 @@ jobs: package-name: Neo.SmartContract.Testing package-type: nuget min-versions-to-keep: 3 + delete-only-pre-release-versions: "true" token: "${{ secrets.GITHUB_TOKEN }}" - name: Delete Neo.SmartContract.Framework Package @@ -24,4 +25,5 @@ jobs: package-name: Neo.Neo.SmartContract.Framework package-type: nuget min-versions-to-keep: 3 + delete-only-pre-release-versions: "true" token: "${{ secrets.GITHUB_TOKEN }}" From 75bc559b1bbe8ebf1734e4f6c9bd7baec37e59e6 Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 19 Feb 2024 13:07:15 +0100 Subject: [PATCH 07/22] Fix Neo.SmartContract.Testing package (#913) --- .../Neo.SmartContract.Testing.csproj | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj b/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj index dd3a11f79..e4af5d8d3 100644 --- a/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj +++ b/src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj @@ -2,14 +2,13 @@ Neo.SmartContract.Testing - netstandard2.1;net7.0 + netstandard2.1;net7.0 latest Neo.SmartContract.Testing NEO;Blockchain;Smart Contract TestEngine for NEO smart contract testing. true - false - content + content enable $(NoWarn);NU5128 From 8434cb20ea3449e438aaae27d659538ebd914c22 Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 19 Feb 2024 13:38:28 +0100 Subject: [PATCH 08/22] Fix publish netstandard in Testing package (#914) --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 389959aef..224e1e666 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -95,9 +95,14 @@ jobs: --configuration Debug \ --output ./out \ --version-suffix ${{ env.VERSION_SUFFIX }} + - name : Build (Neo.SmartContract.Testing) + run: | + dotnet build ./src/Neo.SmartContract.Testing -f net7.0 + dotnet build ./src/Neo.SmartContract.Testing -f netstandard2.1 - name : Pack (Neo.SmartContract.Testing) run: | dotnet pack ./src/Neo.SmartContract.Testing \ + --no-build \ --configuration Debug \ --output ./out \ --version-suffix ${{ env.VERSION_SUFFIX }} From 470d9a8608b41de658849994a258200d8abf7caa Mon Sep 17 00:00:00 2001 From: Shargon Date: Mon, 19 Feb 2024 13:57:33 +0100 Subject: [PATCH 09/22] TestEngine: add Checkpoints, RpcStorage and Dump to html (#904) * Draft Checkpoint * fix namespaces * Add ut * Update src/Neo.SmartContract.Testing/Storage/EngineStorage.cs Co-authored-by: Jimmy * Rpc Storage * Seek * clean * By pass RPC Backward * Change to testnet * Fix bug * clean using * Conflicts * Dump to html * Fix coverage during OnFault * print string when possible * Coverage 100% * fix comment * Update README * Update src/Neo.SmartContract.Testing/TestingApplicationEngine.cs * format * Improve method name * Refactor AbiMethod constructor * Update tests/Neo.SmartContract.Template.UnitTests/templates/neocontractnep17/CoverageContractTests.cs --------- Co-authored-by: Jimmy --- src/Neo.Compiler.CSharp/Options.cs | 2 +- src/Neo.Compiler.CSharp/Program.cs | 4 +- .../Coverage/AbiMethod.cs | 20 +- .../Coverage/CoverageHit.cs | 70 ++++++- .../Coverage/CoveredContract.cs | 157 +++++++++++++--- .../Coverage/CoveredMethod.cs | 9 +- .../Coverage/DumpFormat.cs | 15 ++ .../Extensions/TestExtensions.cs | 1 + src/Neo.SmartContract.Testing/README.md | 97 +++++++++- .../SmartContract.cs | 4 + .../Storage/EngineCheckpoint.cs | 113 ++++++++++++ .../EngineStorage.cs} | 23 ++- .../Storage/Rpc/RpcSnapshot.cs | 64 +++++++ .../Storage/Rpc/RpcStore.cs | 173 ++++++++++++++++++ src/Neo.SmartContract.Testing/TestEngine.cs | 47 ++++- .../TestingApplicationEngine.cs | 26 ++- .../TestingStandards/Nep17Tests.cs | 70 +++++++ .../neocontractnep17/CoverageContractTests.cs | 11 +- .../neocontractnep17/Nep17ContractTests.cs | 6 + .../neocontractnep17/OwnerContractTests.cs | 13 ++ .../Coverage/CoverageDataTests.cs | 58 +++--- .../Extensions/ArtifactExtensionsTests.cs | 2 +- .../Storage/Rpc/RpcStoreTests.cs | 47 +++++ .../{ => Storage}/TestStorageTests.cs | 46 ++++- 24 files changed, 987 insertions(+), 91 deletions(-) create mode 100644 src/Neo.SmartContract.Testing/Coverage/DumpFormat.cs create mode 100644 src/Neo.SmartContract.Testing/Storage/EngineCheckpoint.cs rename src/Neo.SmartContract.Testing/{TestStorage.cs => Storage/EngineStorage.cs} (87%) 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 rename tests/Neo.SmartContract.Testing.UnitTests/{ => Storage}/TestStorageTests.cs (60%) 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()); From 5e95cd509651b5bc4ada24cd9e87e44f85b809c6 Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 20 Feb 2024 13:07:38 +0100 Subject: [PATCH 10/22] Clean myget (#918) * Clean myget * Update pkgs-delete.yml * Update pkgs-delete.yml * Update pkgs-delete.yml * Update main.yml --- .github/workflows/main.yml | 10 +++++- .github/workflows/pkgs-delete.yml | 59 +++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 224e1e666..7ccefe846 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,7 +71,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} - PublishGithub: + PublishPackage: if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/') needs: Test runs-on: ubuntu-latest @@ -114,6 +114,14 @@ jobs: --api-key "${{ secrets.GITHUB_TOKEN }}" \ --disable-buffering \ --no-service-endpoint; + - name: Publish to myGet + working-directory: ./out + run: | + dotnet nuget push * \ + --source https://www.myget.org/F/neo/api/v3/index.json \ + --api-key "${{ secrets.MYGET_TOKEN }}" \ + --disable-buffering \ + --no-service-endpoint; Release: if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/') diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index fee4091b8..7be65925d 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -1,12 +1,65 @@ -name: Nuget Package Cleanup (github) +name: Package Cleanup on: + workflow_dispatch: schedule: - cron: '0 0 * * *' # Run every day at 24:00 jobs: - delete-pkgs: - name: Delete Old Nuget Packages + + delete-myget-pkgs: + name: Delete Old MyGet Packages + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install Requests + run: pip install requests + - name: Get versions below 3.6.0 + env: + MYGET_FEED: 'neo' + PACKAGE_NAMES: 'Neo.SmartContract.Framework,Neo.SmartContract.Testing' + MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }} + run: | + import requests + import os + + def get_versions(feed, package_name, api_key): + url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/versions" + headers = {"X-MyGet-ApiKey": api_key} + response = requests.get(url, headers=headers) + if response.status_code == 200: + versions = response.json() + return [version['Version'] for version in versions['d'] if version['Version'] < '3.6.0'] + else: + return [] + + def delete_version(feed, package_name, version, api_key): + delete_url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{version}" + headers = {"X-MyGet-ApiKey": api_key} + response = requests.delete(delete_url, headers=headers) + return response.status_code == 204 # 204 No Content, success + + feed = os.environ['MYGET_FEED'] + package_names = os.environ['PACKAGE_NAMES'].split(',') + api_key = os.environ['MYGET_API_KEY'] + + for package_name in package_names: + versions_to_delete = get_versions(feed, package_name, api_key) + for version in versions_to_delete: + if delete_version(feed, package_name, version, api_key): + print(f"Deleted version {version} of package {package_name}.") + else: + print(f"Failed to delete version {version} of package {package_name}.") + + shell: python + + delete-git-pkgs: + name: Delete Old Github Packages runs-on: ubuntu-latest steps: From 8f99a18a46055fc412b3110786e7c7e6d95a6123 Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 20 Feb 2024 14:03:32 +0100 Subject: [PATCH 11/22] Update pkgs-delete.yml (#919) * Update pkgs-delete.yml * Update pkgs-delete.yml --- .github/workflows/pkgs-delete.yml | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index 7be65925d..e86fc0f71 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -19,6 +19,8 @@ jobs: python-version: '3.x' - name: Install Requests run: pip install requests + - name: Install Packaging + run: pip install packaging - name: Get versions below 3.6.0 env: MYGET_FEED: 'neo' @@ -26,20 +28,21 @@ jobs: MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }} run: | import requests + from packaging import version import os def get_versions(feed, package_name, api_key): - url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/versions" - headers = {"X-MyGet-ApiKey": api_key} + url = f"https://www.myget.org/F/neo/api/v2/Packages?$filter=Id eq '{package_name}'&$format=json" + headers = {'Accept': 'application/json'} response = requests.get(url, headers=headers) if response.status_code == 200: - versions = response.json() - return [version['Version'] for version in versions['d'] if version['Version'] < '3.6.0'] + versions = response.json()['d']['results'] + return [ver['Version'] for ver in versions] else: return [] - def delete_version(feed, package_name, version, api_key): - delete_url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{version}" + def delete_version(feed, package_name, ver, api_key): + delete_url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}" headers = {"X-MyGet-ApiKey": api_key} response = requests.delete(delete_url, headers=headers) return response.status_code == 204 # 204 No Content, success @@ -50,11 +53,14 @@ jobs: for package_name in package_names: versions_to_delete = get_versions(feed, package_name, api_key) - for version in versions_to_delete: - if delete_version(feed, package_name, version, api_key): - print(f"Deleted version {version} of package {package_name}.") + for ver in versions_to_delete: + if version.parse(ver.split("-", 1)[0]) >= version.Version("3.6.0"): + print(f"Omited {ver} of package {package_name}.") + continue + if delete_version(feed, package_name, ver, api_key): + print(f"Deleted version {ver} of package {package_name}.") else: - print(f"Failed to delete version {version} of package {package_name}.") + print(f"Failed to delete version {ver} of package {package_name}.") shell: python From 6d1325e27163eaa8ee5bf221877103f4aedc37a1 Mon Sep 17 00:00:00 2001 From: Shargon Date: Tue, 20 Feb 2024 23:37:18 +0100 Subject: [PATCH 12/22] Update pkgs-delete.yml (#920) * Update pkgs-delete.yml * Update .github/workflows/pkgs-delete.yml * Update .github/workflows/pkgs-delete.yml --- .github/workflows/pkgs-delete.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index e86fc0f71..d2abe09b3 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -32,7 +32,7 @@ jobs: import os def get_versions(feed, package_name, api_key): - url = f"https://www.myget.org/F/neo/api/v2/Packages?$filter=Id eq '{package_name}'&$format=json" + url = f"https://www.myget.org/F/{feed}/api/v2/Packages?$filter=Id eq '{package_name}'&$format=json" headers = {'Accept': 'application/json'} response = requests.get(url, headers=headers) if response.status_code == 200: @@ -42,9 +42,9 @@ jobs: return [] def delete_version(feed, package_name, ver, api_key): - delete_url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}" - headers = {"X-MyGet-ApiKey": api_key} - response = requests.delete(delete_url, headers=headers) + url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}?hardDelete=true" + headers = {"X-NuGet-ApiKey": api_key} + response = requests.delete(url, headers=headers) return response.status_code == 204 # 204 No Content, success feed = os.environ['MYGET_FEED'] From 7b12533db1ec2b79b99831237d6799fb14f3af12 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 00:09:53 +0100 Subject: [PATCH 13/22] Update pkgs-delete.yml (#922) * Update pkgs-delete.yml * Update pkgs-delete.yml --- .github/workflows/pkgs-delete.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index d2abe09b3..1edc446d9 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -21,10 +21,10 @@ jobs: run: pip install requests - name: Install Packaging run: pip install packaging - - name: Get versions below 3.6.0 + - name: Delete versions below 3.6.1 env: MYGET_FEED: 'neo' - PACKAGE_NAMES: 'Neo.SmartContract.Framework,Neo.SmartContract.Testing' + PACKAGE_NAMES: 'Neo,Neo.VM,Neo.Json,Neo.Cryptography.BLS12_381,Neo.SmartContract.Framework,Neo.SmartContract.Testing' MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }} run: | import requests @@ -32,7 +32,7 @@ jobs: import os def get_versions(feed, package_name, api_key): - url = f"https://www.myget.org/F/{feed}/api/v2/Packages?$filter=Id eq '{package_name}'&$format=json" + url = f"https://www.myget.org/F/{feed}/api/v2/Packages?$select=Version&$filter=Id eq '{package_name}'&$format=json" headers = {'Accept': 'application/json'} response = requests.get(url, headers=headers) if response.status_code == 200: @@ -45,7 +45,7 @@ jobs: url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}?hardDelete=true" headers = {"X-NuGet-ApiKey": api_key} response = requests.delete(url, headers=headers) - return response.status_code == 204 # 204 No Content, success + return response.status_code == 200 # Success feed = os.environ['MYGET_FEED'] package_names = os.environ['PACKAGE_NAMES'].split(',') @@ -54,7 +54,7 @@ jobs: for package_name in package_names: versions_to_delete = get_versions(feed, package_name, api_key) for ver in versions_to_delete: - if version.parse(ver.split("-", 1)[0]) >= version.Version("3.6.0"): + if version.parse(ver.split("-", 1)[0]) >= version.Version("3.6.1"): print(f"Omited {ver} of package {package_name}.") continue if delete_version(feed, package_name, ver, api_key): From ba387348163b7b9d788f672cbfb0e4e09f7b6927 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 00:32:44 +0100 Subject: [PATCH 14/22] Remove big nuget packages (#923) * Remove big nuget packages * Update pkgs-delete.yml * Update .github/workflows/pkgs-delete.yml --- .github/workflows/pkgs-delete.yml | 56 +++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index 1edc446d9..1dd8eab74 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -7,12 +7,62 @@ on: jobs: + delete-myget-big-pkgs: + name: Delete Big MyGet Packages + runs-on: ubuntu-latest + steps: + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install Requests + run: pip install requests + - name: Install Packaging + run: pip install packaging + - name: Delete versions below 3.6.1 + env: + MYGET_FEED: 'neo' + PACKAGE_NAMES: 'bctklib,Neo.Assertions,Neo.BuildTasks,Neo.Collector,Neo.Test.Harness,Neo.Test.Runner,Neo.Trace,Neo.WorkNet,Neo.Json' + MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }} + run: | + import requests + from packaging import version + import os + + def get_versions(feed, package_name, api_key): + url = f"https://www.myget.org/F/{feed}/api/v2/Packages?$select=Version&$filter=Id eq '{package_name}'&$format=json" + headers = {'Accept': 'application/json'} + response = requests.get(url, headers=headers) + if response.status_code == 200: + versions = response.json()['d']['results'] + return [ver['Version'] for ver in versions] + else: + return [] + + def delete_version(feed, package_name, ver, api_key): + url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}?hardDelete=true" + headers = {"X-NuGet-ApiKey": api_key} + response = requests.delete(url, headers=headers) + return response.status_code == 200 # Success + + feed = os.environ['MYGET_FEED'] + package_names = os.environ['PACKAGE_NAMES'].split(',') + api_key = os.environ['MYGET_API_KEY'] + + for package_name in package_names: + versions_to_delete = get_versions(feed, package_name, api_key) + for ver in versions_to_delete: + if delete_version(feed, package_name, ver, api_key): + print(f"Deleted version {ver} of package {package_name}.") + else: + print(f"Failed to delete version {ver} of package {package_name}.") + + shell: python + delete-myget-pkgs: name: Delete Old MyGet Packages runs-on: ubuntu-latest steps: - - name: Check out code - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v4 with: @@ -24,7 +74,7 @@ jobs: - name: Delete versions below 3.6.1 env: MYGET_FEED: 'neo' - PACKAGE_NAMES: 'Neo,Neo.VM,Neo.Json,Neo.Cryptography.BLS12_381,Neo.SmartContract.Framework,Neo.SmartContract.Testing' + PACKAGE_NAMES: 'Neo.SmartContract.Framework,Neo.SmartContract.Testing' MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }} run: | import requests From 2e6378ae10216a3befa79649f9d7e5bd103a0a55 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 00:35:54 +0100 Subject: [PATCH 15/22] Update pkgs-delete.yml (#924) Remove selective remove --- .github/workflows/pkgs-delete.yml | 52 ------------------------------- 1 file changed, 52 deletions(-) diff --git a/.github/workflows/pkgs-delete.yml b/.github/workflows/pkgs-delete.yml index 1dd8eab74..a0ec38710 100644 --- a/.github/workflows/pkgs-delete.yml +++ b/.github/workflows/pkgs-delete.yml @@ -7,58 +7,6 @@ on: jobs: - delete-myget-big-pkgs: - name: Delete Big MyGet Packages - runs-on: ubuntu-latest - steps: - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install Requests - run: pip install requests - - name: Install Packaging - run: pip install packaging - - name: Delete versions below 3.6.1 - env: - MYGET_FEED: 'neo' - PACKAGE_NAMES: 'bctklib,Neo.Assertions,Neo.BuildTasks,Neo.Collector,Neo.Test.Harness,Neo.Test.Runner,Neo.Trace,Neo.WorkNet,Neo.Json' - MYGET_API_KEY: ${{ secrets.MYGET_TOKEN }} - run: | - import requests - from packaging import version - import os - - def get_versions(feed, package_name, api_key): - url = f"https://www.myget.org/F/{feed}/api/v2/Packages?$select=Version&$filter=Id eq '{package_name}'&$format=json" - headers = {'Accept': 'application/json'} - response = requests.get(url, headers=headers) - if response.status_code == 200: - versions = response.json()['d']['results'] - return [ver['Version'] for ver in versions] - else: - return [] - - def delete_version(feed, package_name, ver, api_key): - url = f"https://www.myget.org/F/{feed}/api/v2/package/{package_name}/{ver}?hardDelete=true" - headers = {"X-NuGet-ApiKey": api_key} - response = requests.delete(url, headers=headers) - return response.status_code == 200 # Success - - feed = os.environ['MYGET_FEED'] - package_names = os.environ['PACKAGE_NAMES'].split(',') - api_key = os.environ['MYGET_API_KEY'] - - for package_name in package_names: - versions_to_delete = get_versions(feed, package_name, api_key) - for ver in versions_to_delete: - if delete_version(feed, package_name, ver, api_key): - print(f"Deleted version {ver} of package {package_name}.") - else: - print(f"Failed to delete version {ver} of package {package_name}.") - - shell: python - delete-myget-pkgs: name: Delete Old MyGet Packages runs-on: ubuntu-latest From 83600e24d0d227a881869664ce0e4da195c7bfd8 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 09:22:06 +0100 Subject: [PATCH 16/22] Fix: compilation and coverage issues (#921) * Fix compiler length * Fix method detection * Fix ut transfer * allow to check multiple * Fix IList * Allow CALLT and different methods detection * Clean code * add comment * clean * Fix clean * Allow to release the mock from engine * Release mock * Show error when access to storage without deploy * remove 0x * use params * Dictionary * Update NuGet.Config * Update NuGet.Config Co-authored-by: Jimmy * Update src/Neo.SmartContract.Testing/SmartContractStorage.cs --------- Co-authored-by: Jimmy --- NuGet.Config | 4 +- src/Neo.Compiler.CSharp/CompilationContext.cs | 7 ++ .../Coverage/CoverageHit.cs | 14 ++- .../Coverage/CoveredContract.cs | 111 ++++++++++++++---- .../Coverage/MethodDetectionMechanism.cs | 21 ++++ .../Extensions/TestExtensions.cs | 15 +++ .../SmartContract.cs | 10 +- .../SmartContractStorage.cs | 8 +- src/Neo.SmartContract.Testing/TestEngine.cs | 46 +++++++- .../TestingApplicationEngine.cs | 3 +- .../TestingStandards/Nep17Tests.cs | 17 +++ 11 files changed, 227 insertions(+), 29 deletions(-) create mode 100644 src/Neo.SmartContract.Testing/Coverage/MethodDetectionMechanism.cs diff --git a/NuGet.Config b/NuGet.Config index 63ebbbee9..61b4900b8 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -2,7 +2,7 @@ - + - \ No newline at end of file + diff --git a/src/Neo.Compiler.CSharp/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationContext.cs index 57b3a7653..9f1f6bb57 100644 --- a/src/Neo.Compiler.CSharp/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationContext.cs @@ -266,6 +266,13 @@ public NefFile CreateExecutable() Tokens = methodTokens.ToArray(), Script = Script }; + + if (nef.Compiler.Length > 64) + { + // Neo.Compiler.CSharp 3.6.2+470d9a8608b41de658849994a258200d8abf7caa + nef.Compiler = nef.Compiler.Substring(0, 61) + "..."; + } + nef.CheckSum = NefFile.ComputeChecksum(nef); // Ensure that is serializable return nef.ToArray().AsSerializable(); diff --git a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs index 872299c45..7468cffb0 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoverageHit.cs @@ -127,7 +127,7 @@ public CoverageHit Clone() /// /// Instruction /// Description - public static string DescriptionFromInstruction(Instruction instruction) + public static string DescriptionFromInstruction(Instruction instruction, params MethodToken[]? tokens) { if (instruction.Operand.Length > 0) { @@ -135,6 +135,18 @@ public static string DescriptionFromInstruction(Instruction instruction) switch (instruction.OpCode) { + case OpCode.CALLT: + { + var tokenId = instruction.TokenU16; + + if (tokens != null && tokens.Length > tokenId) + { + var token = tokens[tokenId]; + + return ret + $" ({token.Hash},{token.Method},{token.ParametersCount},{token.CallFlags})"; + } + break; + } case OpCode.JMP: case OpCode.JMPIF: case OpCode.JMPIFNOT: diff --git a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs index a64ffbb62..2047ed253 100644 --- a/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs +++ b/src/Neo.SmartContract.Testing/Coverage/CoveredContract.cs @@ -36,19 +36,26 @@ public class CoveredContract : CoverageBase /// /// CoveredContract /// + /// Method detection mechanism /// Hash - /// Contract abi - /// Script - public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script) + /// Contract state + public CoveredContract(MethodDetectionMechanism mechanism, UInt160 hash, ContractState? state) { Hash = hash; Methods = Array.Empty(); - if (script is null) return; - - // Extract all methods + if (state is not null) + { + // Extract all methods + GenerateMethods(mechanism, state); + } + } - GenerateMethods(abi, script); + internal void GenerateMethods(MethodDetectionMechanism mechanism, ContractState state) + { + Script script = state.Script; + HashSet privateAdded = new(); + List methods = new(state.Manifest.Abi.Methods); // Iterate all valid instructions @@ -57,29 +64,91 @@ public CoveredContract(UInt160 hash, ContractAbi? abi, Script? script) while (ip < script.Length) { var instruction = script.GetInstruction(ip); - _coverageData[ip] = new CoverageHit(ip, CoverageHit.DescriptionFromInstruction(instruction), false); - ip += instruction.Size; - } - } - internal void GenerateMethods(ContractAbi? abi, Script? script) - { - Methods = Array.Empty(); + if (!_coverageData.ContainsKey(ip)) + { + _coverageData[ip] = new CoverageHit(ip, CoverageHit.DescriptionFromInstruction(instruction, state.Nef.Tokens), false); + } + + if (mechanism == MethodDetectionMechanism.NextMethod) + { + // Find private methods + + switch (instruction.OpCode) + { + case OpCode.CALL_L: + { + var offset = ip + instruction.TokenI32; + if (privateAdded.Add(offset)) + { + methods.Add(new ContractMethodDescriptor() + { + Name = "_private" + offset, + Offset = offset, + ReturnType = ContractParameterType.Void, + Safe = false, + Parameters = Array.Empty(), + }); + } + break; + } + case OpCode.CALLT: + { + var offset = ip + instruction.TokenI8; + if (privateAdded.Add(offset)) + { + methods.Add(new ContractMethodDescriptor() + { + Name = "_private" + offset, + Offset = offset, + ReturnType = ContractParameterType.Void, + Safe = false, + Parameters = Array.Empty(), + }); + } + break; + } + } + } - if (script is null || abi is null) return; + ip += instruction.Size; + } - Methods = abi.Methods - .Select(s => CreateMethod(abi, script, s)) + Methods = methods + .Select(s => CreateMethod(mechanism, script, methods, s)) .OrderBy(o => o.Offset) .ToArray()!; } - private CoveredMethod CreateMethod(ContractAbi abi, Script script, ContractMethodDescriptor abiMethod) + private CoveredMethod CreateMethod( + MethodDetectionMechanism mechanism, Script script, + List allMethods, ContractMethodDescriptor abiMethod + ) { + int ip = abiMethod.Offset; var to = script.Length - 1; - var next = abi.Methods.OrderBy(u => u.Offset).Where(u => u.Offset > abiMethod.Offset).FirstOrDefault(); - if (next is not null) to = next.Offset - 1; + switch (mechanism) + { + case MethodDetectionMechanism.FindRET: + { + while (ip < script.Length) + { + var instruction = script.GetInstruction(ip); + if (instruction.OpCode == OpCode.RET) break; + ip += instruction.Size; + to = ip; + } + break; + } + case MethodDetectionMechanism.NextMethod: + case MethodDetectionMechanism.NextMethodInAbi: + { + var next = allMethods.OrderBy(u => u.Offset).Where(u => u.Offset > abiMethod.Offset).FirstOrDefault(); + if (next is not null) to = next.Offset - 1; + break; + } + } // Return method coverage @@ -300,7 +369,7 @@ public void Hit(int instructionPointer, Instruction instruction, long gas) { // Note: This call is unusual, out of the expected - _coverageData[instructionPointer] = coverage = new CoverageHit(instructionPointer, CoverageHit.DescriptionFromInstruction(instruction), true); + _coverageData[instructionPointer] = coverage = new(instructionPointer, CoverageHit.DescriptionFromInstruction(instruction), true); } coverage.Hit(gas); } diff --git a/src/Neo.SmartContract.Testing/Coverage/MethodDetectionMechanism.cs b/src/Neo.SmartContract.Testing/Coverage/MethodDetectionMechanism.cs new file mode 100644 index 000000000..7024fc29f --- /dev/null +++ b/src/Neo.SmartContract.Testing/Coverage/MethodDetectionMechanism.cs @@ -0,0 +1,21 @@ +namespace Neo.SmartContract.Testing.Coverage +{ + public enum MethodDetectionMechanism + { + /// + /// Find RET + /// + FindRET, + + /// + /// Next method defined in Abi + /// If there are any private method, it probably will return undesired results + /// + NextMethodInAbi, + + /// + /// It will compute the private methods + /// + NextMethod + } +} diff --git a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs index b03300d64..427c4ff8c 100644 --- a/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs +++ b/src/Neo.SmartContract.Testing/Extensions/TestExtensions.cs @@ -75,6 +75,9 @@ public static class TestExtensions _ when type == typeof(UInt160) => new UInt160(stackItem.GetSpan().ToArray()), _ when type == typeof(UInt256) => new UInt256(stackItem.GetSpan().ToArray()), _ when type == typeof(ECPoint) => ECPoint.FromBytes(stackItem.GetSpan().ToArray(), ECCurve.Secp256r1), + _ when type == typeof(IDictionary) && stackItem is Map mp => ToDictionary(mp), // SubItems in StackItem type + _ when type == typeof(Dictionary) && stackItem is Map mp => ToDictionary(mp), // SubItems in StackItem type + _ when type == typeof(IList) && stackItem is CompoundType cp => new List(cp.SubItems), // SubItems in StackItem type _ when type == typeof(List) && stackItem is CompoundType cp => new List(cp.SubItems), // SubItems in StackItem type _ when typeof(IInteroperable).IsAssignableFrom(type) => CreateInteroperable(stackItem, type), _ when type.IsArray && stackItem is CompoundType cp => CreateTypeArray(cp.SubItems, type.GetElementType()!), @@ -84,6 +87,18 @@ _ when typeof(IInteroperable).IsAssignableFrom(type) => CreateInteroperable(stac }; } + private static IDictionary ToDictionary(Map map) + { + Dictionary dictionary = new(); + + foreach (var entry in map) + { + dictionary.Add(entry.Key, entry.Value); + } + + return dictionary; + } + private static object CreateTypeArray(IEnumerable objects, Type elementType) { var obj = objects.ToArray(); diff --git a/src/Neo.SmartContract.Testing/SmartContract.cs b/src/Neo.SmartContract.Testing/SmartContract.cs index ed957e3d5..96dc93fe5 100644 --- a/src/Neo.SmartContract.Testing/SmartContract.cs +++ b/src/Neo.SmartContract.Testing/SmartContract.cs @@ -10,7 +10,7 @@ namespace Neo.SmartContract.Testing { - public class SmartContract + public class SmartContract : IDisposable { internal readonly TestEngine Engine; private readonly Type _contractType; @@ -138,5 +138,13 @@ internal void InvokeOnNotify(string eventName, VM.Types.Array state) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static implicit operator UInt160(SmartContract value) => value.Hash; + + /// + /// Release mock + /// + public void Dispose() + { + Engine.ReleaseMock(this); + } } } diff --git a/src/Neo.SmartContract.Testing/SmartContractStorage.cs b/src/Neo.SmartContract.Testing/SmartContractStorage.cs index c23248a1f..9ea108558 100644 --- a/src/Neo.SmartContract.Testing/SmartContractStorage.cs +++ b/src/Neo.SmartContract.Testing/SmartContractStorage.cs @@ -24,7 +24,13 @@ internal SmartContractStorage(SmartContract smartContract, int? contractId = nul private int GetContractId() { // If it was not initialized checking the contract, we need to query the contract id - _contractId ??= _smartContract.Engine.Native.ContractManagement.GetContract(_smartContract.Hash).Id; + + if (_contractId is not null) return _contractId.Value; + + var state = _smartContract.Engine.Native.ContractManagement.GetContract(_smartContract.Hash) + ?? throw new Exception($"The contract {_smartContract.Hash} is not deployed, so it's not possible to get the storage id."); + + _contractId = state.Id; return _contractId.Value; } diff --git a/src/Neo.SmartContract.Testing/TestEngine.cs b/src/Neo.SmartContract.Testing/TestEngine.cs index 7a0843bee..88465d50b 100644 --- a/src/Neo.SmartContract.Testing/TestEngine.cs +++ b/src/Neo.SmartContract.Testing/TestEngine.cs @@ -84,6 +84,11 @@ public class TestEngine /// public bool EnableCoverageCapture { get; set; } = true; + /// + /// Method detection + /// + public MethodDetectionMechanism MethodDetection { get; set; } = MethodDetectionMechanism.FindRET; + /// /// Validators Address /// @@ -301,6 +306,11 @@ public T Deploy(NefFile nef, ContractManifest manifest, object? data = null, var state = Native.ContractManagement.Deploy(nef.ToArray(), Encoding.UTF8.GetBytes(manifest.ToJson().ToString(false)), data); + if (state is null) + { + throw new Exception("Can't get the ContractState"); + } + // Mock contract //UInt160 hash = Helper.GetContractHash(Sender, nef.CheckSum, manifest.Name); @@ -313,7 +323,7 @@ public T Deploy(NefFile nef, ContractManifest manifest, object? data = null, if (EnableCoverageCapture) { var coverage = GetCoverage(ret); - coverage?.GenerateMethods(state.Manifest.Abi, state.Script); + coverage?.GenerateMethods(MethodDetection, state); } return ret; @@ -452,6 +462,38 @@ internal bool TryGetCustomMock(UInt160 hash, string method, int rc, [NotNullWhen return false; } + /// + /// Release custom mock + /// + /// Contract + /// True if a mock was released + public bool ReleaseMock(SmartContract contract) + { + if (_customMocks.TryGetValue(contract.Hash, out var mocks)) + { + // Remove custom mock + + var ret = false; + + foreach (var entry in mocks.ToArray()) + { + if (ReferenceEquals(entry.Value.Contract, contract)) + { + if (mocks.Remove(entry.Key)) ret = true; + } + } + + if (mocks.Count == 0) + { + _customMocks.Remove(contract.Hash); + } + + return ret; + } + + return false; + } + /// /// Execute raw script /// @@ -517,7 +559,7 @@ public StackItem Execute(Script script) var state = Neo.SmartContract.Native.NativeContract.ContractManagement.GetContract(Storage.Snapshot, contract.Hash); if (state == null) return null; - coveredContract = new(contract.Hash, state.Manifest.Abi, state.Script); + coveredContract = new(MethodDetection, contract.Hash, state); Coverage[coveredContract.Hash] = coveredContract; } diff --git a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs index 71ac969d5..2a6f6e6c5 100644 --- a/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs +++ b/src/Neo.SmartContract.Testing/TestingApplicationEngine.cs @@ -78,7 +78,8 @@ private void RecoverCoverage(Instruction instruction) var state = Native.NativeContract.ContractManagement.GetContract(Engine.Storage.Snapshot, contractHash); - Engine.Coverage[contractHash] = coveredContract = new(contractHash, state?.Manifest.Abi, InstructionContext.Script); + coveredContract = new(Engine.MethodDetection, contractHash, state); + Engine.Coverage[contractHash] = coveredContract; } if (InstructionPointer is null) return; diff --git a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs index a2981bf70..710d72f69 100644 --- a/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs +++ b/src/Neo.SmartContract.Testing/TestingStandards/Nep17Tests.cs @@ -6,6 +6,7 @@ using Neo.VM; using Neo.VM.Types; using System.Collections.Generic; +using System.Linq; using System.Numerics; namespace Neo.SmartContract.Testing.TestingStandards; @@ -71,6 +72,21 @@ public void AssertTransferEvent(UInt160? from, UInt160? to, BigInteger? amount) raisedTransfer.Clear(); } + /// + /// Assert that Transfer event was raised + /// + /// From + /// To + /// Amount + public void AssertTransferEvent(params (UInt160? from, UInt160? to, BigInteger? amount)[] events) + { + Assert.AreEqual(events.Length, raisedTransfer.Count); + CollectionAssert.AreEqual(raisedTransfer.Select(u => u.from).ToArray(), events.Select(u => u.from).ToArray()); + CollectionAssert.AreEqual(raisedTransfer.Select(u => u.to).ToArray(), events.Select(u => u.to).ToArray()); + CollectionAssert.AreEqual(raisedTransfer.Select(u => u.amount).ToArray(), events.Select(u => u.amount).ToArray()); + raisedTransfer.Clear(); + } + /// /// Assert that Transfer event was NOT raised /// @@ -120,6 +136,7 @@ public virtual void TestTransfer() var fromBalance = Contract.BalanceOf(Alice.Account); Assert.IsTrue(fromBalance > 5, "Alice needs at least 5 tokens"); + Assert.AreEqual(0, Contract.BalanceOf(Bob.Account), "Bob must have 0 tokens"); Assert.IsTrue(Contract.Transfer(Alice.Account, Bob.Account, 3)); Assert.AreEqual(fromBalance - 3, Contract.BalanceOf(Alice.Account)); From 839235ad8f156abacfedfb0d2b295413c99d2975 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 09:32:20 +0100 Subject: [PATCH 17/22] Fix neo version with submodule (#925) * Fix neo version with submodule * Update submodule --- .github/workflows/main.yml | 8 +++++++- neo | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ccefe846..9dc1dd2ea 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,8 +86,10 @@ jobs: with: dotnet-version: ${{ env.DOTNET_VERSION }} - - name: Set Version + - name: Set Version (dev-pack) run: git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX={}' >> $GITHUB_ENV + - name: Set Version (Neo) + run: git --git-dir=./neo/.git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX_NEO={}' >> $GITHUB_ENV - name : Pack (Neo.SmartContract.Framework) run: | @@ -95,6 +97,10 @@ jobs: --configuration Debug \ --output ./out \ --version-suffix ${{ env.VERSION_SUFFIX }} + - name : Build (Neo) + run: | + dotnet build ./neo -f net7.0 --version-suffix ${{ env.VERSION_SUFFIX_NEO }} + dotnet build ./neo -f netstandard2.1 --version-suffix ${{ env.VERSION_SUFFIX_NEO }} - name : Build (Neo.SmartContract.Testing) run: | dotnet build ./src/Neo.SmartContract.Testing -f net7.0 diff --git a/neo b/neo index 3f002a657..febcc2153 160000 --- a/neo +++ b/neo @@ -1 +1 @@ -Subproject commit 3f002a657bd7272f551eb6d4eafa5552cf0ac88a +Subproject commit febcc2153ccf83ca8118a72ede4d5f05ce8b67ea From 198c3e0261f05cdb2115f269918fe13f88cfe00f Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 09:39:59 +0100 Subject: [PATCH 18/22] Update main.yml (#926) --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9dc1dd2ea..e4e61d665 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -99,8 +99,7 @@ jobs: --version-suffix ${{ env.VERSION_SUFFIX }} - name : Build (Neo) run: | - dotnet build ./neo -f net7.0 --version-suffix ${{ env.VERSION_SUFFIX_NEO }} - dotnet build ./neo -f netstandard2.1 --version-suffix ${{ env.VERSION_SUFFIX_NEO }} + dotnet build ./neo --version-suffix ${{ env.VERSION_SUFFIX_NEO }} - name : Build (Neo.SmartContract.Testing) run: | dotnet build ./src/Neo.SmartContract.Testing -f net7.0 From 04364bc9ff18c9c93bc6c50a65247abad7e58a6e Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 09:59:24 +0100 Subject: [PATCH 19/22] Update main.yml (#927) --- .github/workflows/main.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4e61d665..628158258 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -102,15 +102,14 @@ jobs: dotnet build ./neo --version-suffix ${{ env.VERSION_SUFFIX_NEO }} - name : Build (Neo.SmartContract.Testing) run: | - dotnet build ./src/Neo.SmartContract.Testing -f net7.0 - dotnet build ./src/Neo.SmartContract.Testing -f netstandard2.1 + dotnet build ./src/Neo.SmartContract.Testing --no-dependencies --version-suffix ${{ env.VERSION_SUFFIX }} -f net7.0 + dotnet build ./src/Neo.SmartContract.Testing --no-dependencies --version-suffix ${{ env.VERSION_SUFFIX }} -f netstandard2.1 - name : Pack (Neo.SmartContract.Testing) run: | dotnet pack ./src/Neo.SmartContract.Testing \ --no-build \ --configuration Debug \ - --output ./out \ - --version-suffix ${{ env.VERSION_SUFFIX }} + --output ./out - name: Publish to Github Packages working-directory: ./out run: | From 263aaf5e92541719296c022b42136a68004f344e Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 10:15:03 +0100 Subject: [PATCH 20/22] Update main.yml (#928) * Update main.yml * Update .github/workflows/main.yml * Update .github/workflows/main.yml --- .github/workflows/main.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 628158258..d9818af36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -100,16 +100,21 @@ jobs: - name : Build (Neo) run: | dotnet build ./neo --version-suffix ${{ env.VERSION_SUFFIX_NEO }} + - name : Replace Neo reference by package in Neo.SmartContract.Testing + run: | + dotnet remove ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj reference '..\..\neo\src\Neo\Neo.csproj' + dotnet add ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj package 'Neo' --version ${{ env.VERSION_SUFFIX_NEO }} - name : Build (Neo.SmartContract.Testing) run: | - dotnet build ./src/Neo.SmartContract.Testing --no-dependencies --version-suffix ${{ env.VERSION_SUFFIX }} -f net7.0 - dotnet build ./src/Neo.SmartContract.Testing --no-dependencies --version-suffix ${{ env.VERSION_SUFFIX }} -f netstandard2.1 + dotnet build ./src/Neo.SmartContract.Testing --no-dependencies -f net7.0 + dotnet build ./src/Neo.SmartContract.Testing --no-dependencies -f netstandard2.1 - name : Pack (Neo.SmartContract.Testing) run: | dotnet pack ./src/Neo.SmartContract.Testing \ --no-build \ --configuration Debug \ - --output ./out + --output ./out \ + --version-suffix ${{ env.VERSION_SUFFIX }} - name: Publish to Github Packages working-directory: ./out run: | From ad6a4666f0944ed18ecd02b83a3302895f5f84a9 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 10:27:23 +0100 Subject: [PATCH 21/22] Update main.yml (#929) * Update main.yml * Update main.yml --- .github/workflows/main.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d9818af36..3d89882fc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,17 +97,14 @@ jobs: --configuration Debug \ --output ./out \ --version-suffix ${{ env.VERSION_SUFFIX }} - - name : Build (Neo) - run: | - dotnet build ./neo --version-suffix ${{ env.VERSION_SUFFIX_NEO }} - name : Replace Neo reference by package in Neo.SmartContract.Testing run: | dotnet remove ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj reference '..\..\neo\src\Neo\Neo.csproj' - dotnet add ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj package 'Neo' --version ${{ env.VERSION_SUFFIX_NEO }} + dotnet add ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj package 'Neo' --version 3.6.2-${{ env.VERSION_SUFFIX_NEO }} - name : Build (Neo.SmartContract.Testing) run: | - dotnet build ./src/Neo.SmartContract.Testing --no-dependencies -f net7.0 - dotnet build ./src/Neo.SmartContract.Testing --no-dependencies -f netstandard2.1 + dotnet build ./src/Neo.SmartContract.Testing -f net7.0 + dotnet build ./src/Neo.SmartContract.Testing -f netstandard2.1 - name : Pack (Neo.SmartContract.Testing) run: | dotnet pack ./src/Neo.SmartContract.Testing \ From b2deb4c9aae2d33dec1bba8679d9b0e5afdb1340 Mon Sep 17 00:00:00 2001 From: Shargon Date: Wed, 21 Feb 2024 11:52:36 +0100 Subject: [PATCH 22/22] Update main.yml (#932) --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d89882fc..563169a3d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,9 @@ jobs: - name: Set Version (dev-pack) run: git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX={}' >> $GITHUB_ENV - name: Set Version (Neo) - run: git --git-dir=./neo/.git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX_NEO={}' >> $GITHUB_ENV + run: | + git --git-dir=./neo/.git rev-list --count HEAD | xargs printf 'CI%05d' | xargs -I{} echo 'VERSION_SUFFIX_NEO={}' >> $GITHUB_ENV + sed -n 's/.*\(.*\)<\/VersionPrefix>.*/\1/p' ./neo/src/Directory.Build.props | xargs -I{} echo 'VERSION_NEO={}' >> $GITHUB_ENV - name : Pack (Neo.SmartContract.Framework) run: | @@ -100,7 +102,7 @@ jobs: - name : Replace Neo reference by package in Neo.SmartContract.Testing run: | dotnet remove ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj reference '..\..\neo\src\Neo\Neo.csproj' - dotnet add ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj package 'Neo' --version 3.6.2-${{ env.VERSION_SUFFIX_NEO }} + dotnet add ./src/Neo.SmartContract.Testing/Neo.SmartContract.Testing.csproj package 'Neo' --version ${{ env.VERSION_NEO }}-${{ env.VERSION_SUFFIX_NEO }} - name : Build (Neo.SmartContract.Testing) run: | dotnet build ./src/Neo.SmartContract.Testing -f net7.0