[wasm][debugger] Fixing async locals in nested ContinueWith blocks (#56911)
authorThays Grazia <thaystg@gmail.com>
Fri, 6 Aug 2021 19:54:40 +0000 (16:54 -0300)
committerGitHub <noreply@github.com>
Fri, 6 Aug 2021 19:54:40 +0000 (14:54 -0500)
* Adding test for #41984

* Adding old @radical tests and fixing it.

* Fixing tests.

* Update src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs

Co-authored-by: Ankit Jain <radical@gmail.com>
* Update src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs

Co-authored-by: Ankit Jain <radical@gmail.com>
* Addressing @radical comments.

* Addressing @radical comments.

* Addressing @radical comments.

Co-authored-by: Ankit Jain <radical@gmail.com>
src/mono/wasm/debugger/BrowserDebugProxy/MonoSDBHelper.cs
src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/GetPropertiesTests.cs
src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs
src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test/debugger-test2.cs

index 9b4b51c28596d78369076c44ab5ca6db44ee027f..b508067902105e912130d158d35d12af25b4a09b 100644 (file)
@@ -623,6 +623,7 @@ namespace Microsoft.WebAssembly.Diagnostics
         private static int MINOR_VERSION = 61;
         private static int MAJOR_VERSION = 2;
         private readonly ILogger logger;
+        private Regex regexForAsyncLocals = new Regex(@"\<([^)]*)\>", RegexOptions.Singleline);
 
         public MonoSDBHelper(MonoProxy proxy, ILogger logger)
         {
@@ -1710,6 +1711,62 @@ namespace Microsoft.WebAssembly.Diagnostics
             return retDebuggerCmdReader.ReadByte() == 1 ; //token
         }
 
+        private bool IsClosureReferenceField (string fieldName)
+        {
+            // mcs is "$locvar"
+            // old mcs is "<>f__ref"
+            // csc is "CS$<>"
+            // roslyn is "<>8__"
+            return fieldName.StartsWith ("CS$<>", StringComparison.Ordinal) ||
+                        fieldName.StartsWith ("<>f__ref", StringComparison.Ordinal) ||
+                        fieldName.StartsWith ("$locvar", StringComparison.Ordinal) ||
+                        fieldName.StartsWith ("<>8__", StringComparison.Ordinal);
+        }
+
+        public async Task<JArray> GetHoistedLocalVariables(SessionId sessionId, int objectId, JArray asyncLocals, CancellationToken token)
+        {
+            JArray asyncLocalsFull = new JArray();
+            List<int> objectsAlreadyRead = new();
+            objectsAlreadyRead.Add(objectId);
+            foreach (var asyncLocal in asyncLocals)
+            {
+                var fieldName = asyncLocal["name"].Value<string>();
+                if (fieldName.EndsWith("__this", StringComparison.Ordinal))
+                {
+                    asyncLocal["name"] = "this";
+                    asyncLocalsFull.Add(asyncLocal);
+                }
+                else if (IsClosureReferenceField(fieldName)) //same code that has on debugger-libs
+                {
+                    if (DotnetObjectId.TryParse(asyncLocal?["value"]?["objectId"]?.Value<string>(), out DotnetObjectId dotnetObjectId))
+                    {
+                        if (int.TryParse(dotnetObjectId.Value, out int objectIdToGetInfo) && !objectsAlreadyRead.Contains(objectIdToGetInfo))
+                        {
+                            var asyncLocalsFromObject = await GetObjectValues(sessionId, objectIdToGetInfo, true, false, false, false, token);
+                            var hoistedLocalVariable = await GetHoistedLocalVariables(sessionId, objectIdToGetInfo, asyncLocalsFromObject, token);
+                            asyncLocalsFull = new JArray(asyncLocalsFull.Union(hoistedLocalVariable));
+                        }
+                    }
+                }
+                else if (fieldName.StartsWith("<>", StringComparison.Ordinal)) //examples: <>t__builder, <>1__state
+                {
+                    continue;
+                }
+                else if (fieldName.StartsWith('<')) //examples: <code>5__2
+                {
+                    var match = regexForAsyncLocals.Match(fieldName);
+                    if (match.Success)
+                        asyncLocal["name"] = match.Groups[1].Value;
+                    asyncLocalsFull.Add(asyncLocal);
+                }
+                else
+                {
+                    asyncLocalsFull.Add(asyncLocal);
+                }
+            }
+            return asyncLocalsFull;
+        }
+
         public async Task<JArray> StackFrameGetValues(SessionId sessionId, MethodInfo method, int thread_id, int frame_id, VarInfo[] varIds, CancellationToken token)
         {
             var commandParams = new MemoryStream();
@@ -1729,14 +1786,7 @@ namespace Microsoft.WebAssembly.Diagnostics
                 retDebuggerCmdReader.ReadByte(); //ignore type
                 var objectId = retDebuggerCmdReader.ReadInt32();
                 var asyncLocals = await GetObjectValues(sessionId, objectId, true, false, false, false, token);
-                asyncLocals = new JArray(asyncLocals.Where( asyncLocal => !asyncLocal["name"].Value<string>().Contains("<>") || asyncLocal["name"].Value<string>().EndsWith("__this")));
-                foreach (var asyncLocal in asyncLocals)
-                {
-                    if (asyncLocal["name"].Value<string>().EndsWith("__this"))
-                        asyncLocal["name"] = "this";
-                    else if (asyncLocal["name"].Value<string>().Contains('<'))
-                        asyncLocal["name"] = Regex.Match(asyncLocal["name"].Value<string>(), @"\<([^)]*)\>").Groups[1].Value;
-                }
+                asyncLocals = await GetHoistedLocalVariables(sessionId, objectId, asyncLocals, token);
                 return asyncLocals;
             }
 
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/AsyncTests.cs
new file mode 100644 (file)
index 0000000..9a26136
--- /dev/null
@@ -0,0 +1,82 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.WebAssembly.Diagnostics;
+using Newtonsoft.Json.Linq;
+using Xunit;
+
+namespace DebuggerTests
+{
+    public class AsyncTests : DebuggerTestBase
+    {
+
+        // FIXME: method with multiple async blocks - so that we have two separate classes for that method!
+        // FIXME: nested blocks
+        // FIXME: Confirm the actual bp location
+        // FIXME: check object properties..
+
+        //FIXME: function name
+        [Theory]
+        [InlineData("ContinueWithStaticAsync", "<ContinueWithStaticAsync>b__3_0")]
+        [InlineData("ContinueWithInstanceAsync", "<ContinueWithInstanceAsync>b__5_0")]
+        public async Task AsyncLocalsInContinueWith(string method_name, string expected_method_name) => await CheckInspectLocalsAtBreakpointSite(
+             "DebuggerTests.AsyncTests.ContinueWithTests", method_name, 5, expected_method_name,
+             "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
+             wait_for_event_fn: async (pause_location) =>
+             {
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    t = TObject("System.Threading.Tasks.Task.DelayPromise"),
+                    code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
+                    @this = TObject("DebuggerTests.AsyncTests.ContinueWithTests.<>c"),
+                    dt = TDateTime(new DateTime(4513, 4, 5, 6, 7, 8))
+                }, "locals");
+
+                var res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "t"), "Status");
+                await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status");
+             });
+
+        [Fact]
+        public async Task AsyncLocalsInContinueWithInstanceUsingThisBlock() => await CheckInspectLocalsAtBreakpointSite(
+             "DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithInstanceUsingThisAsync", 5, "<ContinueWithInstanceUsingThisAsync>b__6_0",
+             "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
+             wait_for_event_fn: async (pause_location) =>
+             {
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    t = TObject("System.Threading.Tasks.Task.DelayPromise"),
+                    code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
+                    dt = TDateTime(new DateTime(4513, 4, 5, 6, 7, 8)),
+                    @this = TObject("DebuggerTests.AsyncTests.ContinueWithTests")
+                }, "locals");
+
+                var res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "t"), "Status");
+                await CheckValue(res.Value["result"], TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"), "t.Status");
+
+                res = await InvokeGetter(GetAndAssertObjectWithName(frame_locals, "this"), "Date");
+                await CheckValue(res.Value["result"], TDateTime(new DateTime(2510, 1, 2, 3, 4, 5)), "this.Date");
+             });
+
+         [Fact] // NestedContinueWith
+         public async Task AsyncLocalsInNestedContinueWithStaticBlock() => await CheckInspectLocalsAtBreakpointSite(
+              "DebuggerTests.AsyncTests.ContinueWithTests", "NestedContinueWithStaticAsync", 5, "MoveNext",
+              "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
+              wait_for_event_fn: async (pause_location) =>
+              {
+                 var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                 await CheckProps(frame_locals, new
+                 {
+                     t = TObject("System.Threading.Tasks.Task.DelayPromise"),
+                     code = TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"),
+                     str = TString("foobar"),
+                     @this = TObject("DebuggerTests.AsyncTests.ContinueWithTests.<>c__DisplayClass4_0"),
+                     ncs_dt0 = TDateTime(new DateTime(3412, 4, 6, 8, 0, 2))
+                 }, "locals");
+              });
+    }
+}
index 91f369b71be821403b2fc8bff8423f8ee976f0f5..0b613dcca3370a199562b8bd6068b46ce34d6c9d 100644 (file)
@@ -620,6 +620,27 @@ namespace DebuggerTests
                 (_, res) = await EvaluateOnCallFrame(id, "DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", expect_ok: false);
                 AssertEqual("Failed to resolve member access for DebuggerTests.InvalidEvaluateStaticClass.StaticProperty2", res.Error["result"]?["description"]?.Value<string>(), "wrong error message");
            });
+
+         [Fact]
+         public async Task AsyncLocalsInContinueWithBlock() => await CheckInspectLocalsAtBreakpointSite(
+            "DebuggerTests.AsyncTests.ContinueWithTests", "ContinueWithStaticAsync", 4, "<ContinueWithStaticAsync>b__3_0",
+            "window.setTimeout(function() { invoke_static_method('[debugger-test] DebuggerTests.AsyncTests.ContinueWithTests:RunAsync'); })",
+            wait_for_event_fn: async (pause_location) =>
+            {
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
+
+                await EvaluateOnCallFrameAndCheck(id,
+                    ($"t.Status", TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion")),
+                    ($"  t.Status", TEnum("System.Threading.Tasks.TaskStatus", "RanToCompletion"))
+                );
+
+                await EvaluateOnCallFrameFail(id,
+                    ("str", "ReferenceError"),
+                    ("  str", "ReferenceError")
+                );
+            });
+
     }
 
 }
index da971017a35ad76bcd83cc1c820c0df37f2406f4..d6904e3c746b9efc4fb991ef855a3377d2d378bd 100644 (file)
@@ -340,7 +340,7 @@ namespace DebuggerTests
         {
             var pause_location = await EvaluateAndCheck(
                "window.setTimeout(function() { invoke_static_method('[debugger-test] TestChild:TestWatchWithInheritance'); }, 1);",
-                "dotnet://debugger-test.dll/debugger-test2.cs", 125, 8,
+                "dotnet://debugger-test.dll/debugger-test2.cs", 127, 8,
                "TestWatchWithInheritance");
             var frame_id = pause_location["callFrames"][0]["callFrameId"].Value<string>();
             var frame_locals = await GetProperties(frame_id);
index b09e32a28765ea0cf08f938d9df8bbde956a2af0..038af35dd71f2d8de40af66d8c9d3e5dbf68fc34 100644 (file)
@@ -830,12 +830,13 @@ namespace DebuggerTests
         public async Task InspectTaskAtLocals() => await CheckInspectLocalsAtBreakpointSite(
             "InspectTask",
             "RunInspectTask",
-            7,
+            10,
             "<RunInspectTask>b__0" ,
             $"window.setTimeout(function() {{ invoke_static_method_async('[debugger-test] InspectTask:RunInspectTask'); }}, 1);",
             wait_for_event_fn: async (pause_location) =>
             {
                 var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                CheckNumber(locals, "a", 10);
 
                 var t_props = await GetObjectOnLocals(locals, "t");
                 await CheckProps(t_props, new
diff --git a/src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs b/src/mono/wasm/debugger/tests/debugger-test/debugger-async-test.cs
new file mode 100644 (file)
index 0000000..56fafba
--- /dev/null
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Threading.Tasks;
+using System.Runtime.CompilerServices;
+
+namespace DebuggerTests.AsyncTests
+{
+    public class ContinueWithTests
+    {
+        public DateTime Date => new DateTime(2510, 1, 2, 3, 4, 5);
+
+        public static async Task RunAsync()
+        {
+            await ContinueWithStaticAsync("foobar");
+            await new ContinueWithTests().ContinueWithInstanceAsync("foobar");
+
+            await NestedContinueWithStaticAsync("foobar");
+            await new ContinueWithTests().NestedContinueWithInstanceAsync("foobar");
+            await new ContinueWithTests().ContinueWithInstanceUsingThisAsync("foobar");
+
+        }
+
+        public static async Task ContinueWithStaticAsync(string str)
+        {
+            await Task.Delay(1000).ContinueWith(t =>
+            {
+                var code = t.Status;
+                var dt = new DateTime(4513, 4, 5, 6, 7, 8);
+                Console.WriteLine ($"First continueWith: {code}, {dt}"); //t, code, dt
+            });
+            Console.WriteLine ($"done with this method");
+        }
+
+        public static async Task NestedContinueWithStaticAsync(string str)
+        {
+            await Task.Delay(500).ContinueWith(async t =>
+            {
+                var code = t.Status;
+                var ncs_dt0 = new DateTime(3412, 4, 6, 8, 0, 2);
+                Console.WriteLine ($"First continueWith: {code}, {ncs_dt0}"); // t, code, str, dt0
+                await Task.Delay(300).ContinueWith(t2 =>
+                {
+                    var ncs_dt1 = new DateTime(4513, 4, 5, 6, 7, 8);
+                    Console.WriteLine ($"t2: {t2.Status}, str: {str}, {ncs_dt1}, {ncs_dt0}");//t2, dt1, str, dt0
+                });
+            });
+            Console.WriteLine ($"done with this method");
+        }
+
+        public async Task ContinueWithInstanceAsync(string str)
+        {
+            await Task.Delay(1000).ContinueWith(t =>
+            {
+                var code = t.Status;
+                var dt = new DateTime(4513, 4, 5, 6, 7, 8);
+                Console.WriteLine ($"First continueWith: {code}, {dt}");// t, code, dt
+            });
+            Console.WriteLine ($"done with this method");
+        }
+
+        public async Task ContinueWithInstanceUsingThisAsync(string str)
+        {
+            await Task.Delay(1000).ContinueWith(t =>
+            {
+                var code = t.Status;
+                var dt = new DateTime(4513, 4, 5, 6, 7, 8);
+                Console.WriteLine ($"First continueWith: {code}, {dt}, {this.Date}");
+            });
+            Console.WriteLine ($"done with this method");
+        }
+
+        [MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.NoInlining)]
+        public async Task NestedContinueWithInstanceAsync(string str)
+        {
+            await Task.Delay(500).ContinueWith(async t =>
+            {
+                var code = t.Status;
+                var dt0 = new DateTime(3412, 4, 6, 8, 0, 2);
+                if (str == "oi")
+                {
+                    dt0 = new DateTime(3415, 4, 6, 8, 0, 2);
+                }
+                Console.WriteLine ($"First continueWith: {code}, {dt0}, {Date}");//this, t, code, str, dt0
+                await Task.Delay(300).ContinueWith(t2 =>
+                {
+                    var dt1 = new DateTime(4513, 4, 5, 6, 7, 8);
+                    Console.WriteLine ($"t2: {t2.Status}, str: {str}, {dt1}, {dt0}");//this, t2, dt1, str, dt0
+                });
+            });
+            Console.WriteLine ($"done with this method");
+        }
+
+    }
+
+}
index 8cfb301ff7e51533a9ce3988eb02a2ff7b263c3b..9756cbe26de44958bf839d27c8d6e2a391edbdfa 100644 (file)
@@ -85,6 +85,8 @@ public class InspectTask
         {
             await getJsonTask.ContinueWith(t =>
                 {
+                    int a = 10;
+                    Console.WriteLine(a);
                     if (t.IsCompletedSuccessfully)
                         forecasts = t.Result;