diff --git a/src/Neo.Compiler.CSharp/CompilationContext.cs b/src/Neo.Compiler.CSharp/CompilationContext.cs index 883458ed5..718716c48 100644 --- a/src/Neo.Compiler.CSharp/CompilationContext.cs +++ b/src/Neo.Compiler.CSharp/CompilationContext.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Neo.Compiler.Optimizer; using Neo.Cryptography.ECC; using Neo.IO; using Neo.Json; @@ -134,7 +135,7 @@ internal void Compile() instructions.RebuildOffsets(); if (Options.Optimize.HasFlag(CompilationOptions.OptimizationType.Basic)) { - Optimizer.CompressJumps(instructions); + BasicOptimizer.CompressJumps(instructions); } instructions.RebuildOperands(); } diff --git a/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs b/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs index 8a5160dab..1abe59933 100644 --- a/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs +++ b/src/Neo.Compiler.CSharp/MethodConvert/MethodConvert.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Neo.Compiler.Optimizer; using Neo.Cryptography.ECC; using Neo.IO; using Neo.SmartContract; @@ -238,7 +239,7 @@ public void Convert(SemanticModel model) AddInstruction(OpCode.RET); } if (_context.Options.Optimize.HasFlag(CompilationOptions.OptimizationType.Basic)) - Optimizer.RemoveNops(_instructions); + BasicOptimizer.RemoveNops(_instructions); _startTarget.Instruction = _instructions[0]; } diff --git a/src/Neo.Compiler.CSharp/Optimizer.cs b/src/Neo.Compiler.CSharp/Optimizer/BasicOptimizer.cs similarity index 91% rename from src/Neo.Compiler.CSharp/Optimizer.cs rename to src/Neo.Compiler.CSharp/Optimizer/BasicOptimizer.cs index 38230cc9d..a4854d19e 100644 --- a/src/Neo.Compiler.CSharp/Optimizer.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/BasicOptimizer.cs @@ -11,9 +11,9 @@ using Neo.VM; using System.Collections.Generic; -namespace Neo.Compiler +namespace Neo.Compiler.Optimizer { - static class Optimizer + static class BasicOptimizer { public static void RemoveNops(List instructions) { @@ -57,8 +57,8 @@ public static void CompressJumps(IReadOnlyList instructions) } if (instruction.OpCode == OpCode.TRY_L) { - int offset1 = (instruction.Target.Instruction?.Offset - instruction.Offset) ?? 0; - int offset2 = (instruction.Target2!.Instruction?.Offset - instruction.Offset) ?? 0; + int offset1 = instruction.Target.Instruction?.Offset - instruction.Offset ?? 0; + int offset2 = instruction.Target2!.Instruction?.Offset - instruction.Offset ?? 0; if (offset1 >= sbyte.MinValue && offset1 <= sbyte.MaxValue && offset2 >= sbyte.MinValue && offset2 <= sbyte.MaxValue) { compressed = true; diff --git a/src/Neo.Compiler.CSharp/Optimizer/DumpNef.cs b/src/Neo.Compiler.CSharp/Optimizer/DumpNef.cs index 7e84e3e3b..b4e9c044e 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/DumpNef.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/DumpNef.cs @@ -12,7 +12,7 @@ namespace Neo.Optimizer { - public static class DumpNef + static class DumpNef { #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. private static readonly Regex DocumentRegex = new(@"\[(\d+)\](\d+)\:(\d+)\-(\d+)\:(\d+)", RegexOptions.Compiled); diff --git a/src/Neo.Compiler.CSharp/Optimizer/JumpTarget.cs b/src/Neo.Compiler.CSharp/Optimizer/JumpTarget.cs index 354ddf067..b8f5b92a9 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/JumpTarget.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/JumpTarget.cs @@ -1,16 +1,14 @@ using Neo.SmartContract; using Neo.VM; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using static Neo.Optimizer.OpCodeTypes; using static Neo.VM.OpCode; namespace Neo.Optimizer { - public static class JumpTarget + static class JumpTarget { public static bool SingleJumpInOperand(Instruction instruction) => SingleJumpInOperand(instruction.OpCode); public static bool SingleJumpInOperand(OpCode opcode) @@ -56,36 +54,60 @@ public static (int catchTarget, int finallyTarget) ComputeTryTarget(int addr, In }; } - public static (ConcurrentDictionary, - ConcurrentDictionary) + public static (Dictionary, + Dictionary, + Dictionary>) FindAllJumpAndTrySourceToTargets(NefFile nef) { Script script = nef.Script; return FindAllJumpAndTrySourceToTargets(script); } - public static (ConcurrentDictionary, - ConcurrentDictionary) + public static (Dictionary, + Dictionary, + Dictionary>) FindAllJumpAndTrySourceToTargets(Script script) => FindAllJumpAndTrySourceToTargets(script.EnumerateInstructions().ToList()); - public static (ConcurrentDictionary, - ConcurrentDictionary) + public static ( + Dictionary, // jump source to target + Dictionary, // try source to targets + Dictionary> // target to source + ) FindAllJumpAndTrySourceToTargets(List<(int, Instruction)> addressAndInstructionsList) { Dictionary addressToInstruction = new(); foreach ((int a, Instruction i) in addressAndInstructionsList) addressToInstruction.Add(a, i); - ConcurrentDictionary jumpSourceToTargets = new(); - ConcurrentDictionary trySourceToTargets = new(); - Parallel.ForEach(addressAndInstructionsList, item => + Dictionary jumpSourceToTargets = new(); + Dictionary trySourceToTargets = new(); + Dictionary> targetToSources = new(); + foreach ((int a, Instruction i) in addressAndInstructionsList) { - (int a, Instruction i) = (item.Item1, item.Item2); if (SingleJumpInOperand(i)) - jumpSourceToTargets.TryAdd(i, addressToInstruction[ComputeJumpTarget(a, i)]); + { + Instruction target = addressToInstruction[ComputeJumpTarget(a, i)]; + jumpSourceToTargets.TryAdd(i, target); + if (!targetToSources.TryGetValue(target, out HashSet? sources)) sources = new(); + sources.Add(i); + } if (i.OpCode == TRY) - trySourceToTargets.TryAdd(i, (addressToInstruction[a + i.TokenI8], addressToInstruction[a + i.TokenI8_1])); + { + (Instruction t1, Instruction t2) = (addressToInstruction[a + i.TokenI8], addressToInstruction[a + i.TokenI8_1]); + trySourceToTargets.TryAdd(i, (t1, t2)); + if (!targetToSources.TryGetValue(t1, out HashSet? sources1)) sources1 = new(); + sources1.Add(i); + if (!targetToSources.TryGetValue(t2, out HashSet? sources2)) sources2 = new(); + sources2.Add(i); + } if (i.OpCode == TRY_L) - trySourceToTargets.TryAdd(i, (addressToInstruction[a + i.TokenI32], addressToInstruction[a + i.TokenI32_1])); - }); - return (jumpSourceToTargets, trySourceToTargets); + { + (Instruction t1, Instruction t2) = (addressToInstruction[a + i.TokenI32], addressToInstruction[a + i.TokenI32_1]); + trySourceToTargets.TryAdd(i, (t1, t2)); + if (!targetToSources.TryGetValue(t1, out HashSet? sources1)) sources1 = new(); + sources1.Add(i); + if (!targetToSources.TryGetValue(t2, out HashSet? sources2)) sources2 = new(); + sources2.Add(i); + } + } + return (jumpSourceToTargets, trySourceToTargets, targetToSources); } } } diff --git a/src/Neo.Compiler.CSharp/Optimizer/OpCodeTypes.cs b/src/Neo.Compiler.CSharp/Optimizer/OpCodeTypes.cs index 9c662d6aa..a063b70c5 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/OpCodeTypes.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/OpCodeTypes.cs @@ -4,7 +4,7 @@ namespace Neo.Optimizer { - public static class OpCodeTypes + static class OpCodeTypes { public static readonly HashSet push = new(); diff --git a/src/Neo.Compiler.CSharp/Optimizer/Strategies/Optimizer.cs b/src/Neo.Compiler.CSharp/Optimizer/Strategies/Optimizer.cs index c8ac20549..fc3a3b71c 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/Strategies/Optimizer.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/Strategies/Optimizer.cs @@ -8,7 +8,7 @@ namespace Neo.Optimizer { - public class Optimizer + class Optimizer { public static readonly int[] OperandSizePrefixTable = new int[256]; public static readonly int[] OperandSizeTable = new int[256]; diff --git a/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs b/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs index dc8f37e4e..9edb6aab4 100644 --- a/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs +++ b/src/Neo.Compiler.CSharp/Optimizer/Strategies/Reachability.cs @@ -4,18 +4,16 @@ using Neo.VM; using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; -using System.Threading.Tasks; using static Neo.Optimizer.JumpTarget; using static Neo.Optimizer.OpCodeTypes; using static Neo.Optimizer.Optimizer; namespace Neo.Optimizer { - public static class Reachability + static class Reachability { #pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'. private static readonly Regex RangeRegex = new(@"(\d+)\-(\d+)", RegexOptions.Compiled); @@ -36,22 +34,25 @@ public enum BranchType OK, // One of the branches may return without exception THROW, // All branches surely has exceptions, but can be catched ABORT, // All branches abort, and cannot be catched + UNCOVERED, } [Strategy(Priority = int.MaxValue)] public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(NefFile nef, ContractManifest manifest, JObject debugInfo) { - Dictionary coveredMap = FindCoveredInstructions(nef, manifest, debugInfo); + Dictionary coveredMap = FindCoveredInstructions(nef, manifest, debugInfo); Script oldScript = nef.Script; List<(int, Instruction)> oldAddressAndInstructionsList = oldScript.EnumerateInstructions().ToList(); Dictionary oldAddressToInstruction = new(); foreach ((int a, Instruction i) in oldAddressAndInstructionsList) oldAddressToInstruction.Add(a, i); + //DumpNef.GenerateDumpNef(nef, debugInfo); + //coveredMap.Where(kv => !kv.Value).Select(kv => (kv.Key, oldAddressToInstruction[kv.Key].OpCode)).ToList(); System.Collections.Specialized.OrderedDictionary simplifiedInstructionsToAddress = new(); int currentAddress = 0; foreach ((int a, Instruction i) in oldAddressAndInstructionsList) { - if (coveredMap[a]) + if (coveredMap[a] != BranchType.UNCOVERED) { simplifiedInstructionsToAddress.Add(i, currentAddress); currentAddress += i.Size; @@ -59,8 +60,8 @@ public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(N else continue; } - (ConcurrentDictionary jumpInstructionSourceToTargets, - ConcurrentDictionary tryInstructionSourceToTargets) + (Dictionary jumpInstructionSourceToTargets, + Dictionary tryInstructionSourceToTargets, _) = FindAllJumpAndTrySourceToTargets(oldAddressAndInstructionsList); List simplifiedScript = new(); @@ -70,10 +71,15 @@ public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(N simplifiedScript.Add((byte)i.OpCode); int operandSizeLength = OperandSizePrefixTable[(int)i.OpCode]; simplifiedScript = simplifiedScript.Concat(BitConverter.GetBytes(i.Operand.Length)[0..operandSizeLength]).ToList(); - if (jumpInstructionSourceToTargets.ContainsKey(i)) + if (jumpInstructionSourceToTargets.TryGetValue(i, out Instruction? dst)) { - Instruction dst = jumpInstructionSourceToTargets[i]; - int delta = (int)simplifiedInstructionsToAddress[dst]! - a; + int delta; + if (simplifiedInstructionsToAddress.Contains(dst)) // target instruction not deleted + delta = (int)simplifiedInstructionsToAddress[dst]! - a; + else if (i.OpCode == OpCode.PUSHA) + delta = 0; // TODO: decide a good target + else + throw new BadScriptException($"Target instruction of {i.OpCode} at address {a} is deleted"); if (i.OpCode == OpCode.JMP || conditionalJump.Contains(i.OpCode) || i.OpCode == OpCode.CALL || i.OpCode == OpCode.ENDTRY) simplifiedScript.Add(BitConverter.GetBytes(delta)[0]); if (i.OpCode == OpCode.PUSHA || i.OpCode == OpCode.JMP_L || conditionalJump_L.Contains(i.OpCode) || i.OpCode == OpCode.CALL_L || i.OpCode == OpCode.ENDTRY_L) @@ -112,7 +118,14 @@ public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(N continue; } int methodStart = (int)simplifiedInstructionsToAddress[oldAddressToInstruction[oldMethodStart]]!; - int methodEnd = (int)simplifiedInstructionsToAddress[oldAddressToInstruction[oldMethodEnd]]!; + // The instruction at the end of the method may have been deleted. + // We need to find the last instruction that is not deleted. + //int methodEnd = (int)simplifiedInstructionsToAddress[oldAddressToInstruction[oldMethodEnd]]!; + int oldMethodEndNotDeleted = oldAddressToInstruction.Where(kv => + kv.Key >= oldMethodStart && kv.Key <= oldMethodEnd && + simplifiedInstructionsToAddress.Contains(kv.Value) + ).Max(kv => kv.Key); + int methodEnd = (int)simplifiedInstructionsToAddress[oldAddressToInstruction[oldMethodEndNotDeleted]]!; method["range"] = $"{methodStart}-{methodEnd}"; int previousSequencePoint = methodStart; @@ -146,23 +159,24 @@ public static (NefFile, ContractManifest, JObject) RemoveUncoveredInstructions(N return (nef, manifest, debugInfo); } - public static Dictionary + public static Dictionary FindCoveredInstructions(NefFile nef, ContractManifest manifest, JToken debugInfo) { Script script = nef.Script; - Dictionary coveredMap = new(); - foreach ((int addr, Instruction inst) in script.EnumerateInstructions()) - coveredMap.Add(addr, false); + Dictionary coveredMap = new(); + foreach ((int addr, Instruction _) in script.EnumerateInstructions()) + coveredMap.Add(addr, BranchType.UNCOVERED); Dictionary publicMethodStartingAddressToName = new(); foreach (ContractMethodDescriptor method in manifest.Abi.Methods) publicMethodStartingAddressToName.Add(method.Offset, method.Name); - Parallel.ForEach(manifest.Abi.Methods, method => - CoverInstruction(method.Offset, script, coveredMap) - ); - //foreach (ContractMethodDescriptor method in manifest.Abi.Methods) - // CoverInstruction(method.Offset, script, coveredMap); + // It is unsafe to go parallel, because the coveredMap value is not true/false + //Parallel.ForEach(manifest.Abi.Methods, method => + // CoverInstruction(method.Offset, script, coveredMap) + //); + foreach (ContractMethodDescriptor method in manifest.Abi.Methods) + CoverInstruction(method.Offset, script, coveredMap); // start from _deploy method foreach (JToken? method in (JArray)debugInfo["methods"]!) { @@ -187,8 +201,9 @@ public static Dictionary /// Whether it is possible to return without exception /// /// - public static BranchType CoverInstruction(int addr, Script script, Dictionary coveredMap, Stack<((int returnAddr, int finallyAddr), TryStack stackType)>? stack = null, bool throwed = false) + public static BranchType CoverInstruction(int addr, Script script, Dictionary coveredMap, Stack<((int returnAddr, int finallyAddr), TryStack stackType)>? stack = null, bool throwed = false) { + int entranceAddr = addr; stack ??= new(); if (stack.Count == 0) stack.Push(((-1, -1), TryStack.ENTRY)); @@ -202,7 +217,7 @@ public static BranchType CoverInstruction(int addr, Script script, Dictionary 0); + while (stackType != TryStack.TRY && stackType != TryStack.CATCH && stack.Count > 0); if (stackType == TryStack.TRY) // goto CATCH or FINALLY { throwed = false; @@ -230,12 +245,12 @@ public static BranchType CoverInstruction(int addr, Script script, Dictionary THROW goto HANDLE_THROW; @@ -379,7 +418,8 @@ public static BranchType CoverInstruction(int addr, Script script, Dictionary /// Strategy name diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Abort.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Abort.cs index 873b5caaf..aae2b1d59 100644 --- a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Abort.cs +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Abort.cs @@ -10,6 +10,8 @@ namespace Neo.Compiler.CSharp.UnitTests public class UnitTest_Abort { TestEngine testengine; + bool[] falseTrue = new bool[] { false, true }; + [TestInitialize] public void Initialize() { @@ -20,39 +22,65 @@ public void Initialize() [TestMethod] public void Test_Abort() { - Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbort").Count, 0); - List AbortAddresses = DumpNef.OpCodeAddressesInMethod(testengine.Nef, testengine.DebugInfo, "testAbort", OpCode.ABORT); - Assert.AreEqual(testengine.CurrentContext.InstructionPointer, AbortAddresses[0]); - Assert.AreEqual(testengine.CurrentContext.LocalVariables[0].GetInteger(), 0); + string method = "testAbort"; + Assert.AreEqual(testengine.ExecuteTestCaseStandard(method).Count, 0); + // All the ABORT instruction addresses in "testAbort" method + List AbortAddresses = DumpNef.OpCodeAddressesInMethod(testengine.Nef, testengine.DebugInfo, method, OpCode.ABORT); + Assert.AreEqual(testengine.CurrentContext.InstructionPointer, AbortAddresses[0]); // stop at the 1st ABORT + Assert.AreEqual(testengine.CurrentContext.LocalVariables[0].GetInteger(), 0); // v==0 Assert.AreEqual(testengine.State, VMState.FAULT); } [TestMethod] - public void Test_AbortInFunction() + public void Test_AbortMsg() { - Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInFunction").Count, 0); + string method = "testAbortMsg"; + Assert.AreEqual(testengine.ExecuteTestCaseStandard(method).Count, 0); + // All the ABORTMSG instruction addresses in "testAbortMsg" method + List AbortAddresses = DumpNef.OpCodeAddressesInMethod(testengine.Nef, testengine.DebugInfo, method, OpCode.ABORTMSG); + Assert.AreEqual(testengine.CurrentContext.InstructionPointer, AbortAddresses[0]); // stop at the 1st ABORTMSG + Assert.AreEqual(testengine.CurrentContext.LocalVariables[0].GetInteger(), 0); // v==0 Assert.AreEqual(testengine.State, VMState.FAULT); } + [TestMethod] + public void Test_AbortInFunction() + { + foreach (bool b in falseTrue) + { + Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInFunction", b).Count, 0); + Assert.AreEqual(testengine.State, VMState.FAULT); + } + } + [TestMethod] public void Test_AbortInTry() { - Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInTry").Count, 0); - Assert.AreEqual(testengine.State, VMState.FAULT); + foreach (bool b in falseTrue) + { + Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInTry", b).Count, 0); + Assert.AreEqual(testengine.State, VMState.FAULT); + } } [TestMethod] public void Test_AbortInCatch() { - Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInCatch").Count, 0); - Assert.AreEqual(testengine.State, VMState.FAULT); + foreach (bool b in falseTrue) + { + Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInCatch", b).Count, 0); + Assert.AreEqual(testengine.State, VMState.FAULT); + } } [TestMethod] public void Test_AbortInFinally() { - Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInFinally").Count, 0); - Assert.AreEqual(testengine.State, VMState.FAULT); + foreach (bool b in falseTrue) + { + Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAbortInFinally", b).Count, 0); + Assert.AreEqual(testengine.State, VMState.FAULT); + } } } } diff --git a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Assert.cs b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Assert.cs index 0a60bc91b..04be33252 100644 --- a/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Assert.cs +++ b/tests/Neo.Compiler.CSharp.UnitTests/UnitTest_Assert.cs @@ -19,9 +19,10 @@ public void Initialize() public void AssertsInFalse() { + // All the ASSERT opcode addresses in method "testAssertFalse" List assertAddresses = DumpNef.OpCodeAddressesInMethod(testengine.Nef, testengine.DebugInfo, "testAssertFalse", OpCode.ASSERT); - Assert.AreEqual(testengine.CurrentContext.InstructionPointer, assertAddresses[1]); - Assert.AreEqual(testengine.CurrentContext.LocalVariables[0].GetInteger(), 1); + Assert.AreEqual(testengine.CurrentContext.InstructionPointer, assertAddresses[1]); // stops at the 2nd ASSERT + Assert.AreEqual(testengine.CurrentContext.LocalVariables[0].GetInteger(), 1); // v==1 Assert.AreEqual(testengine.State, VMState.FAULT); } @@ -37,7 +38,7 @@ public void Test_AssertInFunction() { Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAssertInFunction").Count, 0); AssertsInFalse(); - Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 0); + Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 0); // v==0 } [TestMethod] @@ -45,7 +46,7 @@ public void Test_AssertInTry() { Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAssertInTry").Count, 0); AssertsInFalse(); - Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 0); + Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 0); // v==0 } [TestMethod] @@ -53,7 +54,7 @@ public void Test_AssertInCatch() { Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAssertInCatch").Count, 0); AssertsInFalse(); - Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 1); + Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 1); // v==1 } [TestMethod] @@ -61,7 +62,7 @@ public void Test_AssertInFinally() { Assert.AreEqual(testengine.ExecuteTestCaseStandard("testAssertInFinally").Count, 0); AssertsInFalse(); - Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 1); + Assert.AreEqual(testengine.InvocationStack.ToArray()[1].LocalVariables[0].GetInteger(), 1); // v==1 } } }