[wasm] Moving wasm debugger to dotnet/runtime (#40146)
authorThays Grazia <thaystg@gmail.com>
Wed, 5 Aug 2020 22:34:41 +0000 (19:34 -0300)
committerGitHub <noreply@github.com>
Wed, 5 Aug 2020 22:34:41 +0000 (19:34 -0300)
* Moving Wasm debugger to dotnet/runtime

The build is working.
The tests are running.
- make -C src/mono/wasm/ run-debugger-tests

41 files changed:
src/mono/wasm/Makefile
src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugHost/Program.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugHost/Startup.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugHost/TestHarnessStartup.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/AssemblyInfo.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.sln [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs [new file with mode: 0644]
src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/InspectorClient.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/Support.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs [new file with mode: 0644]
src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-array-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-cfo-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-datetime-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-driver.html [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-evaluate-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-pointers-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test.csproj [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-test2.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/dependency.cs [new file with mode: 0644]
src/mono/wasm/debugger/tests/other.js [new file with mode: 0644]
src/mono/wasm/debugger/tests/runtime-debugger.js [new file with mode: 0644]
src/mono/wasm/runtime/library_mono.js
tools-local/tasks/mobile.tasks/WasmAppBuilder/WasmAppBuilder.cs

index 978bd1c..398bd0e 100644 (file)
@@ -139,3 +139,27 @@ run-tests-jsc-%:
 
 run-tests-%:
        PATH="$(JSVU):$(PATH)" $(DOTNET) build $(TOP)/src/libraries/$*/tests/ /t:Test /p:TargetOS=Browser /p:TargetArchitecture=wasm /p:Configuration=$(CONFIG) $(MSBUILD_ARGS)
+
+build-debugger-test-app: 
+       $(DOTNET) build --configuration debug --nologo /p:TargetArchitecture=wasm /p:TargetOS=Browser /p:Configuration=Debug /p:RuntimeConfiguration=$(CONFIG) $(TOP)/src/mono/wasm/debugger/tests
+       cp $(TOP)/src/mono/wasm/debugger/tests/debugger-driver.html $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+       cp $(TOP)/src/mono/wasm/debugger/tests/other.js $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+       cp $(TOP)/src/mono/wasm/debugger/tests/runtime-debugger.js $(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish
+
+run-debugger-tests: build-debugger-test-app build-dbg-testsuite
+       if [ ! -z "$(TEST_FILTER)" ]; then \
+               export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish; \
+               export LC_ALL=en_US.UTF-8; \
+               $(DOTNET) test  $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite --filter FullyQualifiedName~$(TEST_FILTER); \
+               unset TEST_SUITE_PATH LC_ALL; \
+       else \
+               export TEST_SUITE_PATH=$(TOP)/src/mono/wasm/debugger/tests/bin/Debug/publish; \
+               export LC_ALL=en_US.UTF-8; \
+               $(DOTNET) test  $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite $(TEST_ARGS); \
+               unset TEST_SUITE_PATH LC_ALL; \
+       fi
+
+build-dbg-proxy:
+       $(DOTNET) build $(TOP)/src/mono/wasm/debugger/BrowserDebugHost
+build-dbg-testsuite:
+       $(DOTNET) build $(TOP)/src/mono/wasm/debugger/DebuggerTestSuite
diff --git a/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj b/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj
new file mode 100644 (file)
index 0000000..9a038a0
--- /dev/null
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp3.0</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\BrowserDebugProxy\BrowserDebugProxy.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/mono/wasm/debugger/BrowserDebugHost/Program.cs b/src/mono/wasm/debugger/BrowserDebugHost/Program.cs
new file mode 100644 (file)
index 0000000..c45b9ce
--- /dev/null
@@ -0,0 +1,92 @@
+// 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    public class ProxyOptions
+    {
+        public Uri DevToolsUrl { get; set; } = new Uri("http://localhost:9222");
+    }
+
+    public class TestHarnessOptions : ProxyOptions
+    {
+        public string ChromePath { get; set; }
+        public string AppPath { get; set; }
+        public string PagePath { get; set; }
+        public string NodeApp { get; set; }
+    }
+
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            var host = new WebHostBuilder()
+                .UseSetting("UseIISIntegration", false.ToString())
+                .UseKestrel()
+                .UseContentRoot(Directory.GetCurrentDirectory())
+                .UseStartup<Startup>()
+                .ConfigureAppConfiguration((hostingContext, config) =>
+                {
+                    config.AddCommandLine(args);
+                })
+                .UseUrls("http://localhost:9300")
+                .Build();
+
+            host.Run();
+        }
+    }
+
+    public class TestHarnessProxy
+    {
+        static IWebHost host;
+        static Task hostTask;
+        static CancellationTokenSource cts = new CancellationTokenSource();
+        static object proxyLock = new object();
+
+        public static readonly Uri Endpoint = new Uri("http://localhost:9400");
+
+        public static Task Start(string chromePath, string appPath, string pagePath)
+        {
+            lock(proxyLock)
+            {
+                if (host != null)
+                    return hostTask;
+
+                host = WebHost.CreateDefaultBuilder()
+                    .UseSetting("UseIISIntegration", false.ToString())
+                    .ConfigureAppConfiguration((hostingContext, config) =>
+                    {
+                        config.AddEnvironmentVariables(prefix: "WASM_TESTS_");
+                    })
+                    .ConfigureServices((ctx, services) =>
+                    {
+                        services.Configure<TestHarnessOptions>(ctx.Configuration);
+                        services.Configure<TestHarnessOptions>(options =>
+                        {
+                            options.ChromePath = options.ChromePath ?? chromePath;
+                            options.AppPath = appPath;
+                            options.PagePath = pagePath;
+                            options.DevToolsUrl = new Uri("http://localhost:0");
+                        });
+                    })
+                    .UseStartup<TestHarnessStartup>()
+                    .UseUrls(Endpoint.ToString())
+                    .Build();
+                hostTask = host.StartAsync(cts.Token);
+            }
+
+            Console.WriteLine("WebServer Ready!");
+            return hostTask;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs b/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs
new file mode 100644 (file)
index 0000000..16292f9
--- /dev/null
@@ -0,0 +1,164 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    internal class Startup
+    {
+        // This method gets called by the runtime. Use this method to add services to the container.
+        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+        public void ConfigureServices(IServiceCollection services) =>
+            services.AddRouting()
+            .Configure<ProxyOptions>(Configuration);
+
+        public Startup(IConfiguration configuration) =>
+            Configuration = configuration;
+
+        public IConfiguration Configuration { get; }
+
+        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+        public void Configure(IApplicationBuilder app, IOptionsMonitor<ProxyOptions> optionsAccessor, IWebHostEnvironment env)
+        {
+            var options = optionsAccessor.CurrentValue;
+            app.UseDeveloperExceptionPage()
+                .UseWebSockets()
+                .UseDebugProxy(options);
+        }
+    }
+
+    static class DebugExtensions
+    {
+        public static Dictionary<string, string> MapValues(Dictionary<string, string> response, HttpContext context, Uri debuggerHost)
+        {
+            var filtered = new Dictionary<string, string>();
+            var request = context.Request;
+
+            foreach (var key in response.Keys)
+            {
+                switch (key)
+                {
+                    case "devtoolsFrontendUrl":
+                        var front = response[key];
+                        filtered[key] = $"{debuggerHost.Scheme}://{debuggerHost.Authority}{front.Replace ($"ws={debuggerHost.Authority}", $"ws={request.Host}")}";
+                        break;
+                    case "webSocketDebuggerUrl":
+                        var page = new Uri(response[key]);
+                        filtered[key] = $"{page.Scheme}://{request.Host}{page.PathAndQuery}";
+                        break;
+                    default:
+                        filtered[key] = response[key];
+                        break;
+                }
+            }
+            return filtered;
+        }
+
+        public static IApplicationBuilder UseDebugProxy(this IApplicationBuilder app, ProxyOptions options) =>
+            UseDebugProxy(app, options, MapValues);
+
+        public static IApplicationBuilder UseDebugProxy(
+            this IApplicationBuilder app,
+            ProxyOptions options,
+            Func<Dictionary<string, string>, HttpContext, Uri, Dictionary<string, string>> mapFunc)
+        {
+            var devToolsHost = options.DevToolsUrl;
+            app.UseRouter(router =>
+            {
+                router.MapGet("/", Copy);
+                router.MapGet("/favicon.ico", Copy);
+                router.MapGet("json", RewriteArray);
+                router.MapGet("json/list", RewriteArray);
+                router.MapGet("json/version", RewriteSingle);
+                router.MapGet("json/new", RewriteSingle);
+                router.MapGet("devtools/page/{pageId}", ConnectProxy);
+                router.MapGet("devtools/browser/{pageId}", ConnectProxy);
+
+                string GetEndpoint(HttpContext context)
+                {
+                    var request = context.Request;
+                    var requestPath = request.Path;
+                    return $"{devToolsHost.Scheme}://{devToolsHost.Authority}{request.Path}{request.QueryString}";
+                }
+
+                async Task Copy(HttpContext context)
+                {
+                    using(var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) })
+                    {
+                        var response = await httpClient.GetAsync(GetEndpoint(context));
+                        context.Response.ContentType = response.Content.Headers.ContentType.ToString();
+                        if ((response.Content.Headers.ContentLength ?? 0) > 0)
+                            context.Response.ContentLength = response.Content.Headers.ContentLength;
+                        var bytes = await response.Content.ReadAsByteArrayAsync();
+                        await context.Response.Body.WriteAsync(bytes);
+
+                    }
+                }
+
+                async Task RewriteSingle(HttpContext context)
+                {
+                    var version = await ProxyGetJsonAsync<Dictionary<string, string>>(GetEndpoint(context));
+                    context.Response.ContentType = "application/json";
+                    await context.Response.WriteAsync(
+                        JsonSerializer.Serialize(mapFunc(version, context, devToolsHost)));
+                }
+
+                async Task RewriteArray(HttpContext context)
+                {
+                    var tabs = await ProxyGetJsonAsync<Dictionary<string, string>[]>(GetEndpoint(context));
+                    var alteredTabs = tabs.Select(t => mapFunc(t, context, devToolsHost)).ToArray();
+                    context.Response.ContentType = "application/json";
+                    await context.Response.WriteAsync(JsonSerializer.Serialize(alteredTabs));
+                }
+
+                async Task ConnectProxy(HttpContext context)
+                {
+                    if (!context.WebSockets.IsWebSocketRequest)
+                    {
+                        context.Response.StatusCode = 400;
+                        return;
+                    }
+
+                    var endpoint = new Uri($"ws://{devToolsHost.Authority}{context.Request.Path.ToString ()}");
+                    try
+                    {
+                        using var loggerFactory = LoggerFactory.Create(
+                            builder => builder.AddConsole().AddFilter(null, LogLevel.Information));
+                        var proxy = new DebuggerProxy(loggerFactory);
+                        var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
+
+                        await proxy.Run(endpoint, ideSocket);
+                    }
+                    catch (Exception e)
+                    {
+                        Console.WriteLine("got exception {0}", e);
+                    }
+                }
+            });
+            return app;
+        }
+
+        static async Task<T> ProxyGetJsonAsync<T>(string url)
+        {
+            using(var httpClient = new HttpClient())
+            {
+                var response = await httpClient.GetAsync(url);
+                return await JsonSerializer.DeserializeAsync<T>(await response.Content.ReadAsStreamAsync());
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugHost/TestHarnessStartup.cs b/src/mono/wasm/debugger/BrowserDebugHost/TestHarnessStartup.cs
new file mode 100644 (file)
index 0000000..953eb86
--- /dev/null
@@ -0,0 +1,255 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Net.Http;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    public class TestHarnessStartup
+    {
+        static Regex parseConnection = new Regex(@"listening on (ws?s://[^\s]*)");
+        public TestHarnessStartup(IConfiguration configuration)
+        {
+            Configuration = configuration;
+        }
+
+        public IConfiguration Configuration { get; set; }
+
+        // This method gets called by the runtime. Use this method to add services to the container.
+        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services.AddRouting()
+                .Configure<TestHarnessOptions>(Configuration);
+        }
+
+        async Task SendNodeVersion(HttpContext context)
+        {
+            Console.WriteLine("hello chrome! json/version");
+            var resp_obj = new JObject();
+            resp_obj["Browser"] = "node.js/v9.11.1";
+            resp_obj["Protocol-Version"] = "1.1";
+
+            var response = resp_obj.ToString();
+            await context.Response.WriteAsync(response, new CancellationTokenSource().Token);
+        }
+
+        async Task SendNodeList(HttpContext context)
+        {
+            Console.WriteLine("hello chrome! json/list");
+            try
+            {
+                var response = new JArray(JObject.FromObject(new
+                {
+                    description = "node.js instance",
+                        devtoolsFrontendUrl = "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=localhost:9300/91d87807-8a81-4f49-878c-a5604103b0a4",
+                        faviconUrl = "https://nodejs.org/static/favicon.ico",
+                        id = "91d87807-8a81-4f49-878c-a5604103b0a4",
+                        title = "foo.js",
+                        type = "node",
+                        webSocketDebuggerUrl = "ws://localhost:9300/91d87807-8a81-4f49-878c-a5604103b0a4"
+                })).ToString();
+
+                Console.WriteLine($"sending: {response}");
+                await context.Response.WriteAsync(response, new CancellationTokenSource().Token);
+            }
+            catch (Exception e) { Console.WriteLine(e); }
+        }
+
+        public async Task LaunchAndServe(ProcessStartInfo psi, HttpContext context, Func<string, Task<string>> extract_conn_url)
+        {
+
+            if (!context.WebSockets.IsWebSocketRequest)
+            {
+                context.Response.StatusCode = 400;
+                return;
+            }
+
+            var tcs = new TaskCompletionSource<string>();
+
+            var proc = Process.Start(psi);
+            try
+            {
+                proc.ErrorDataReceived += (sender, e) =>
+                {
+                    var str = e.Data;
+                    Console.WriteLine($"stderr: {str}");
+
+                    if (tcs.Task.IsCompleted)
+                        return;
+
+                    var match = parseConnection.Match(str);
+                    if (match.Success)
+                    {
+                        tcs.TrySetResult(match.Groups[1].Captures[0].Value);
+                    }
+                };
+
+                proc.OutputDataReceived += (sender, e) =>
+                {
+                    Console.WriteLine($"stdout: {e.Data}");
+                };
+
+                proc.BeginErrorReadLine();
+                proc.BeginOutputReadLine();
+
+                if (await Task.WhenAny(tcs.Task, Task.Delay(5000)) != tcs.Task)
+                {
+                    Console.WriteLine("Didnt get the con string after 5s.");
+                    throw new Exception("node.js timedout");
+                }
+                var line = await tcs.Task;
+                var con_str = extract_conn_url != null ? await extract_conn_url(line) : line;
+
+                Console.WriteLine($"launching proxy for {con_str}");
+
+                using var loggerFactory = LoggerFactory.Create(
+                    builder => builder.AddConsole().AddFilter(null, LogLevel.Information));
+                var proxy = new DebuggerProxy(loggerFactory);
+                var browserUri = new Uri(con_str);
+                var ideSocket = await context.WebSockets.AcceptWebSocketAsync();
+
+                await proxy.Run(browserUri, ideSocket);
+                Console.WriteLine("Proxy done");
+            }
+            catch (Exception e)
+            {
+                Console.WriteLine("got exception {0}", e);
+            }
+            finally
+            {
+                proc.CancelErrorRead();
+                proc.CancelOutputRead();
+                proc.Kill();
+                proc.WaitForExit();
+                proc.Close();
+            }
+        }
+
+        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+        public void Configure(IApplicationBuilder app, IOptionsMonitor<TestHarnessOptions> optionsAccessor, IWebHostEnvironment env)
+        {
+            app.UseWebSockets();
+            app.UseStaticFiles();
+
+            TestHarnessOptions options = optionsAccessor.CurrentValue;
+
+            var provider = new FileExtensionContentTypeProvider();
+            provider.Mappings[".wasm"] = "application/wasm";
+
+            app.UseStaticFiles(new StaticFileOptions
+            {
+                FileProvider = new PhysicalFileProvider(options.AppPath),
+                    ServeUnknownFileTypes = true, //Cuz .wasm is not a known file type :cry:
+                    RequestPath = "",
+                    ContentTypeProvider = provider
+            });
+
+            var devToolsUrl = options.DevToolsUrl;
+            app.UseRouter(router =>
+            {
+                router.MapGet("launch-chrome-and-connect", async context =>
+                {
+                    Console.WriteLine("New test request");
+                    try
+                    {
+                        var client = new HttpClient();
+                        var psi = new ProcessStartInfo();
+
+                        psi.Arguments = $"--headless --disable-gpu --lang=en-US --incognito --remote-debugging-port={devToolsUrl.Port} http://{TestHarnessProxy.Endpoint.Authority}/{options.PagePath}";
+                        psi.UseShellExecute = false;
+                        psi.FileName = options.ChromePath;
+                        psi.RedirectStandardError = true;
+                        psi.RedirectStandardOutput = true;
+
+                        await LaunchAndServe(psi, context, async(str) =>
+                        {
+                            var start = DateTime.Now;
+                            JArray obj = null;
+
+                            while (true)
+                            {
+                                // Unfortunately it does look like we have to wait
+                                // for a bit after getting the response but before
+                                // making the list request.  We get an empty result
+                                // if we make the request too soon.
+                                await Task.Delay(100);
+
+                                var res = await client.GetStringAsync(new Uri(new Uri(str), "/json/list"));
+                                Console.WriteLine("res is {0}", res);
+
+                                if (!String.IsNullOrEmpty(res))
+                                {
+                                    // Sometimes we seem to get an empty array `[ ]`
+                                    obj = JArray.Parse(res);
+                                    if (obj != null && obj.Count >= 1)
+                                        break;
+                                }
+
+                                var elapsed = DateTime.Now - start;
+                                if (elapsed.Milliseconds > 5000)
+                                {
+                                    Console.WriteLine($"Unable to get DevTools /json/list response in {elapsed.Seconds} seconds, stopping");
+                                    return null;
+                                }
+                            }
+
+                            var wsURl = obj[0] ? ["webSocketDebuggerUrl"]?.Value<string>();
+                            Console.WriteLine(">>> {0}", wsURl);
+
+                            return wsURl;
+                        });
+                    }
+                    catch (Exception ex)
+                    {
+                        Console.WriteLine($"launch-chrome-and-connect failed with {ex.ToString ()}");
+                    }
+                });
+            });
+
+            if (options.NodeApp != null)
+            {
+                Console.WriteLine($"Doing the nodejs: {options.NodeApp}");
+                var nodeFullPath = Path.GetFullPath(options.NodeApp);
+                Console.WriteLine(nodeFullPath);
+                var psi = new ProcessStartInfo();
+
+                psi.UseShellExecute = false;
+                psi.RedirectStandardError = true;
+                psi.RedirectStandardOutput = true;
+
+                psi.Arguments = $"--inspect-brk=localhost:0 {nodeFullPath}";
+                psi.FileName = "node";
+
+                app.UseRouter(router =>
+                {
+                    //Inspector API for using chrome devtools directly
+                    router.MapGet("json", SendNodeList);
+                    router.MapGet("json/list", SendNodeList);
+                    router.MapGet("json/version", SendNodeVersion);
+                    router.MapGet("launch-done-and-connect", async context =>
+                    {
+                        await LaunchAndServe(psi, context, null);
+                    });
+                });
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/AssemblyInfo.cs b/src/mono/wasm/debugger/BrowserDebugProxy/AssemblyInfo.cs
new file mode 100644 (file)
index 0000000..5d9d173
--- /dev/null
@@ -0,0 +1,4 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj
new file mode 100644 (file)
index 0000000..4063416
--- /dev/null
@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.1</TargetFramework>
+    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging" Version="3.1.2" />
+    <PackageReference Include="Mono.Cecil" Version="0.11.2" />
+    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
+    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.5.0" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.sln b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.sln
new file mode 100644 (file)
index 0000000..cfb208d
--- /dev/null
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28407.52
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrowserDebugHost", "..\BrowserDebugHost\BrowserDebugHost.csproj", "{954F768A-23E6-4B14-90E0-27EA6B41FBCC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BrowserDebugProxy", "BrowserDebugProxy.csproj", "{490128B6-9F21-46CA-878A-F22BCF51EF3C}"
+EndProject
+Global
+       GlobalSection(SolutionConfigurationPlatforms) = preSolution
+               Debug|Any CPU = Debug|Any CPU
+               Release|Any CPU = Release|Any CPU
+       EndGlobalSection
+       GlobalSection(ProjectConfigurationPlatforms) = postSolution
+               {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {954F768A-23E6-4B14-90E0-27EA6B41FBCC}.Release|Any CPU.Build.0 = Release|Any CPU
+               {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+               {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+               {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+               {490128B6-9F21-46CA-878A-F22BCF51EF3C}.Release|Any CPU.Build.0 = Release|Any CPU
+       EndGlobalSection
+       GlobalSection(SolutionProperties) = preSolution
+               HideSolutionNode = FALSE
+       EndGlobalSection
+       GlobalSection(ExtensibilityGlobals) = postSolution
+               SolutionGuid = {F8BA2C2D-8F28-4F9E-9C54-51E394EF941E}
+       EndGlobalSection
+EndGlobal
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs
new file mode 100644 (file)
index 0000000..aedcfb2
--- /dev/null
@@ -0,0 +1,883 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Security.Cryptography;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Mono.Cecil.Pdb;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    internal class BreakpointRequest
+    {
+        public string Id { get; private set; }
+        public string Assembly { get; private set; }
+        public string File { get; private set; }
+        public int Line { get; private set; }
+        public int Column { get; private set; }
+        public MethodInfo Method { get; private set; }
+
+        JObject request;
+
+        public bool IsResolved => Assembly != null;
+        public List<Breakpoint> Locations { get; } = new List<Breakpoint>();
+
+        public override string ToString() => $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}";
+
+        public object AsSetBreakpointByUrlResponse(IEnumerable<object> jsloc) => new { breakpointId = Id, locations = Locations.Select(l => l.Location.AsLocation()).Concat(jsloc) };
+
+        public BreakpointRequest()
+        { }
+
+        public BreakpointRequest(string id, MethodInfo method)
+        {
+            Id = id;
+            Method = method;
+        }
+
+        public BreakpointRequest(string id, JObject request)
+        {
+            Id = id;
+            this.request = request;
+        }
+
+        public static BreakpointRequest Parse(string id, JObject args)
+        {
+            return new BreakpointRequest(id, args);
+        }
+
+        public BreakpointRequest Clone() => new BreakpointRequest { Id = Id, request = request };
+
+        public bool IsMatch(SourceFile sourceFile)
+        {
+            var url = request?["url"]?.Value<string>();
+            if (url == null)
+            {
+                var urlRegex = request?["urlRegex"].Value<string>();
+                var regex = new Regex(urlRegex);
+                return regex.IsMatch(sourceFile.Url.ToString()) || regex.IsMatch(sourceFile.DocUrl);
+            }
+
+            return sourceFile.Url.ToString() == url || sourceFile.DotNetUrl == url;
+        }
+
+        public bool TryResolve(SourceFile sourceFile)
+        {
+            if (!IsMatch(sourceFile))
+                return false;
+
+            var line = request?["lineNumber"]?.Value<int>();
+            var column = request?["columnNumber"]?.Value<int>();
+
+            if (line == null || column == null)
+                return false;
+
+            Assembly = sourceFile.AssemblyName;
+            File = sourceFile.DebuggerFileName;
+            Line = line.Value;
+            Column = column.Value;
+            return true;
+        }
+
+        public bool TryResolve(DebugStore store)
+        {
+            if (request == null || store == null)
+                return false;
+
+            return store.AllSources().FirstOrDefault(source => TryResolve(source)) != null;
+        }
+    }
+
+    internal class VarInfo
+    {
+        public VarInfo(VariableDebugInformation v)
+        {
+            this.Name = v.Name;
+            this.Index = v.Index;
+        }
+
+        public VarInfo(ParameterDefinition p)
+        {
+            this.Name = p.Name;
+            this.Index = (p.Index + 1) * -1;
+        }
+
+        public string Name { get; }
+        public int Index { get; }
+
+        public override string ToString() => $"(var-info [{Index}] '{Name}')";
+    }
+
+    internal class CliLocation
+    {
+        public CliLocation(MethodInfo method, int offset)
+        {
+            Method = method;
+            Offset = offset;
+        }
+
+        public MethodInfo Method { get; }
+        public int Offset { get; }
+    }
+
+    internal class SourceLocation
+    {
+        SourceId id;
+        int line;
+        int column;
+        CliLocation cliLoc;
+
+        public SourceLocation(SourceId id, int line, int column)
+        {
+            this.id = id;
+            this.line = line;
+            this.column = column;
+        }
+
+        public SourceLocation(MethodInfo mi, SequencePoint sp)
+        {
+            this.id = mi.SourceId;
+            this.line = sp.StartLine - 1;
+            this.column = sp.StartColumn - 1;
+            this.cliLoc = new CliLocation(mi, sp.Offset);
+        }
+
+        public SourceId Id { get => id; }
+        public int Line { get => line; }
+        public int Column { get => column; }
+        public CliLocation CliLocation => this.cliLoc;
+
+        public override string ToString() => $"{id}:{Line}:{Column}";
+
+        public static SourceLocation Parse(JObject obj)
+        {
+            if (obj == null)
+                return null;
+
+            if (!SourceId.TryParse(obj["scriptId"]?.Value<string>(), out var id))
+                return null;
+
+            var line = obj["lineNumber"]?.Value<int>();
+            var column = obj["columnNumber"]?.Value<int>();
+            if (id == null || line == null || column == null)
+                return null;
+
+            return new SourceLocation(id, line.Value, column.Value);
+        }
+
+        internal class LocationComparer : EqualityComparer<SourceLocation>
+        {
+            public override bool Equals(SourceLocation l1, SourceLocation l2)
+            {
+                if (l1 == null && l2 == null)
+                    return true;
+                else if (l1 == null || l2 == null)
+                    return false;
+
+                return (l1.Line == l2.Line &&
+                    l1.Column == l2.Column &&
+                    l1.Id == l2.Id);
+            }
+
+            public override int GetHashCode(SourceLocation loc)
+            {
+                int hCode = loc.Line ^ loc.Column;
+                return loc.Id.GetHashCode() ^ hCode.GetHashCode();
+            }
+        }
+
+        internal object AsLocation() => new
+        {
+            scriptId = id.ToString(),
+            lineNumber = line,
+            columnNumber = column
+        };
+    }
+
+    internal class SourceId
+    {
+        const string Scheme = "dotnet://";
+
+        readonly int assembly, document;
+
+        public int Assembly => assembly;
+        public int Document => document;
+
+        internal SourceId(int assembly, int document)
+        {
+            this.assembly = assembly;
+            this.document = document;
+        }
+
+        public SourceId(string id)
+        {
+            if (!TryParse(id, out assembly, out document))
+                throw new ArgumentException("invalid source identifier", nameof(id));
+        }
+
+        public static bool TryParse(string id, out SourceId source)
+        {
+            source = null;
+            if (!TryParse(id, out var assembly, out var document))
+                return false;
+
+            source = new SourceId(assembly, document);
+            return true;
+        }
+
+        static bool TryParse(string id, out int assembly, out int document)
+        {
+            assembly = document = 0;
+            if (id == null || !id.StartsWith(Scheme, StringComparison.Ordinal))
+                return false;
+
+            var sp = id.Substring(Scheme.Length).Split('_');
+            if (sp.Length != 2)
+                return false;
+
+            if (!int.TryParse(sp[0], out assembly))
+                return false;
+
+            if (!int.TryParse(sp[1], out document))
+                return false;
+
+            return true;
+        }
+
+        public override string ToString() => $"{Scheme}{assembly}_{document}";
+
+        public override bool Equals(object obj)
+        {
+            if (obj == null)
+                return false;
+            SourceId that = obj as SourceId;
+            return that.assembly == this.assembly && that.document == this.document;
+        }
+
+        public override int GetHashCode() => assembly.GetHashCode() ^ document.GetHashCode();
+
+        public static bool operator ==(SourceId a, SourceId b) => ((object) a == null) ? (object) b == null : a.Equals(b);
+
+        public static bool operator !=(SourceId a, SourceId b) => !a.Equals(b);
+    }
+
+    internal class MethodInfo
+    {
+        MethodDefinition methodDef;
+        SourceFile source;
+
+        public SourceId SourceId => source.SourceId;
+
+        public string Name => methodDef.Name;
+        public MethodDebugInformation DebugInformation => methodDef.DebugInformation;
+
+        public SourceLocation StartLocation { get; }
+        public SourceLocation EndLocation { get; }
+        public AssemblyInfo Assembly { get; }
+        public uint Token => methodDef.MetadataToken.RID;
+
+        public MethodInfo(AssemblyInfo assembly, MethodDefinition methodDef, SourceFile source)
+        {
+            this.Assembly = assembly;
+            this.methodDef = methodDef;
+            this.source = source;
+
+            var sps = DebugInformation.SequencePoints;
+            if (sps == null || sps.Count() < 1)
+                return;
+
+            SequencePoint start = sps[0];
+            SequencePoint end = sps[0];
+
+            foreach (var sp in sps)
+            {
+                if (sp.StartLine < start.StartLine)
+                    start = sp;
+                else if (sp.StartLine == start.StartLine && sp.StartColumn < start.StartColumn)
+                    start = sp;
+
+                if (sp.EndLine > end.EndLine)
+                    end = sp;
+                else if (sp.EndLine == end.EndLine && sp.EndColumn > end.EndColumn)
+                    end = sp;
+            }
+
+            StartLocation = new SourceLocation(this, start);
+            EndLocation = new SourceLocation(this, end);
+        }
+
+        public SourceLocation GetLocationByIl(int pos)
+        {
+            SequencePoint prev = null;
+            foreach (var sp in DebugInformation.SequencePoints)
+            {
+                if (sp.Offset > pos)
+                    break;
+                prev = sp;
+            }
+
+            if (prev != null)
+                return new SourceLocation(this, prev);
+
+            return null;
+        }
+
+        public VarInfo[] GetLiveVarsAt(int offset)
+        {
+            var res = new List<VarInfo>();
+
+            res.AddRange(methodDef.Parameters.Select(p => new VarInfo(p)));
+            res.AddRange(methodDef.DebugInformation.GetScopes()
+                .Where(s => s.Start.Offset <= offset && (s.End.IsEndOfMethod || s.End.Offset > offset))
+                .SelectMany(s => s.Variables)
+                .Where(v => !v.IsDebuggerHidden)
+                .Select(v => new VarInfo(v)));
+
+            return res.ToArray();
+        }
+
+        public override string ToString() => "MethodInfo(" + methodDef.FullName + ")";
+    }
+
+    internal class TypeInfo
+    {
+        AssemblyInfo assembly;
+        TypeDefinition type;
+        List<MethodInfo> methods;
+
+        public TypeInfo(AssemblyInfo assembly, TypeDefinition type)
+        {
+            this.assembly = assembly;
+            this.type = type;
+            methods = new List<MethodInfo>();
+        }
+
+        public string Name => type.Name;
+        public string FullName => type.FullName;
+        public List<MethodInfo> Methods => methods;
+
+        public override string ToString() => "TypeInfo('" + FullName + "')";
+    }
+
+    class AssemblyInfo
+    {
+        static int next_id;
+        ModuleDefinition image;
+        readonly int id;
+        readonly ILogger logger;
+        Dictionary<uint, MethodInfo> methods = new Dictionary<uint, MethodInfo>();
+        Dictionary<string, string> sourceLinkMappings = new Dictionary<string, string>();
+        Dictionary<string, TypeInfo> typesByName = new Dictionary<string, TypeInfo>();
+        readonly List<SourceFile> sources = new List<SourceFile>();
+        internal string Url { get; }
+
+        public AssemblyInfo(IAssemblyResolver resolver, string url, byte[] assembly, byte[] pdb)
+        {
+            this.id = Interlocked.Increment(ref next_id);
+
+            try
+            {
+                Url = url;
+                ReaderParameters rp = new ReaderParameters( /*ReadingMode.Immediate*/ );
+                rp.AssemblyResolver = resolver;
+                // set ReadSymbols = true unconditionally in case there
+                // is an embedded pdb then handle ArgumentException
+                // and assume that if pdb == null that is the cause
+                rp.ReadSymbols = true;
+                rp.SymbolReaderProvider = new PdbReaderProvider();
+                if (pdb != null)
+                    rp.SymbolStream = new MemoryStream(pdb);
+                rp.ReadingMode = ReadingMode.Immediate;
+
+                this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
+            }
+            catch (BadImageFormatException ex)
+            {
+                logger.LogWarning($"Failed to read assembly as portable PDB: {ex.Message}");
+            }
+            catch (ArgumentException)
+            {
+                // if pdb == null this is expected and we
+                // read the assembly without symbols below
+                if (pdb != null)
+                    throw;
+            }
+
+            if (this.image == null)
+            {
+                ReaderParameters rp = new ReaderParameters( /*ReadingMode.Immediate*/ );
+                rp.AssemblyResolver = resolver;
+                if (pdb != null)
+                {
+                    rp.ReadSymbols = true;
+                    rp.SymbolReaderProvider = new PdbReaderProvider();
+                    rp.SymbolStream = new MemoryStream(pdb);
+                }
+
+                rp.ReadingMode = ReadingMode.Immediate;
+
+                this.image = ModuleDefinition.ReadModule(new MemoryStream(assembly), rp);
+            }
+
+            Populate();
+        }
+
+        public AssemblyInfo(ILogger logger)
+        {
+            this.logger = logger;
+        }
+
+        void Populate()
+        {
+            ProcessSourceLink();
+
+            var d2s = new Dictionary<Document, SourceFile>();
+
+            SourceFile FindSource(Document doc)
+            {
+                if (doc == null)
+                    return null;
+
+                if (d2s.TryGetValue(doc, out var source))
+                    return source;
+
+                var src = new SourceFile(this, sources.Count, doc, GetSourceLinkUrl(doc.Url));
+                sources.Add(src);
+                d2s[doc] = src;
+                return src;
+            };
+
+            foreach (var type in image.GetTypes())
+            {
+                var typeInfo = new TypeInfo(this, type);
+                typesByName[type.FullName] = typeInfo;
+
+                foreach (var method in type.Methods)
+                {
+                    foreach (var sp in method.DebugInformation.SequencePoints)
+                    {
+                        var source = FindSource(sp.Document);
+                        var methodInfo = new MethodInfo(this, method, source);
+                        methods[method.MetadataToken.RID] = methodInfo;
+                        if (source != null)
+                            source.AddMethod(methodInfo);
+
+                        typeInfo.Methods.Add(methodInfo);
+                    }
+                }
+            }
+        }
+
+        private void ProcessSourceLink()
+        {
+            var sourceLinkDebugInfo = image.CustomDebugInformations.FirstOrDefault(i => i.Kind == CustomDebugInformationKind.SourceLink);
+
+            if (sourceLinkDebugInfo != null)
+            {
+                var sourceLinkContent = ((SourceLinkDebugInformation) sourceLinkDebugInfo).Content;
+
+                if (sourceLinkContent != null)
+                {
+                    var jObject = JObject.Parse(sourceLinkContent) ["documents"];
+                    sourceLinkMappings = JsonConvert.DeserializeObject<Dictionary<string, string>>(jObject.ToString());
+                }
+            }
+        }
+
+        private Uri GetSourceLinkUrl(string document)
+        {
+            if (sourceLinkMappings.TryGetValue(document, out string url))
+                return new Uri(url);
+
+            foreach (var sourceLinkDocument in sourceLinkMappings)
+            {
+                string key = sourceLinkDocument.Key;
+
+                if (Path.GetFileName(key) != "*")
+                {
+                    continue;
+                }
+
+                var keyTrim = key.TrimEnd('*');
+
+                if (document.StartsWith(keyTrim, StringComparison.OrdinalIgnoreCase))
+                {
+                    var docUrlPart = document.Replace(keyTrim, "");
+                    return new Uri(sourceLinkDocument.Value.TrimEnd('*') + docUrlPart);
+                }
+            }
+
+            return null;
+        }
+
+        public IEnumerable<SourceFile> Sources => this.sources;
+
+        public Dictionary<string, TypeInfo> TypesByName => this.typesByName;
+        public int Id => id;
+        public string Name => image.Name;
+
+        public SourceFile GetDocById(int document)
+        {
+            return sources.FirstOrDefault(s => s.SourceId.Document == document);
+        }
+
+        public MethodInfo GetMethodByToken(uint token)
+        {
+            methods.TryGetValue(token, out var value);
+            return value;
+        }
+
+        public TypeInfo GetTypeByName(string name)
+        {
+            typesByName.TryGetValue(name, out var res);
+            return res;
+        }
+    }
+
+    internal class SourceFile
+    {
+        Dictionary<uint, MethodInfo> methods;
+        AssemblyInfo assembly;
+        int id;
+        Document doc;
+
+        internal SourceFile(AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri)
+        {
+            this.methods = new Dictionary<uint, MethodInfo>();
+            this.SourceLinkUri = sourceLinkUri;
+            this.assembly = assembly;
+            this.id = id;
+            this.doc = doc;
+            this.DebuggerFileName = doc.Url.Replace("\\", "/").Replace(":", "");
+
+            this.SourceUri = new Uri((Path.IsPathRooted(doc.Url) ? "file://" : "") + doc.Url, UriKind.RelativeOrAbsolute);
+            if (SourceUri.IsFile && File.Exists(SourceUri.LocalPath))
+            {
+                this.Url = this.SourceUri.ToString();
+            }
+            else
+            {
+                this.Url = DotNetUrl;
+            }
+        }
+
+        internal void AddMethod(MethodInfo mi)
+        {
+            if (!this.methods.ContainsKey(mi.Token))
+                this.methods[mi.Token] = mi;
+        }
+
+        public string DebuggerFileName { get; }
+        public string Url { get; }
+        public string AssemblyName => assembly.Name;
+        public string DotNetUrl => $"dotnet://{assembly.Name}/{DebuggerFileName}";
+
+        public SourceId SourceId => new SourceId(assembly.Id, this.id);
+        public Uri SourceLinkUri { get; }
+        public Uri SourceUri { get; }
+
+        public IEnumerable<MethodInfo> Methods => this.methods.Values;
+
+        public string DocUrl => doc.Url;
+
+        public(int startLine, int startColumn, int endLine, int endColumn) GetExtents()
+        {
+            var start = Methods.OrderBy(m => m.StartLocation.Line).ThenBy(m => m.StartLocation.Column).First();
+            var end = Methods.OrderByDescending(m => m.EndLocation.Line).ThenByDescending(m => m.EndLocation.Column).First();
+            return (start.StartLocation.Line, start.StartLocation.Column, end.EndLocation.Line, end.EndLocation.Column);
+        }
+
+        async Task<MemoryStream> GetDataAsync(Uri uri, CancellationToken token)
+        {
+            var mem = new MemoryStream();
+            try
+            {
+                if (uri.IsFile && File.Exists(uri.LocalPath))
+                {
+                    using(var file = File.Open(SourceUri.LocalPath, FileMode.Open))
+                    {
+                        await file.CopyToAsync(mem, token).ConfigureAwait(false);
+                        mem.Position = 0;
+                    }
+                }
+                else if (uri.Scheme == "http" || uri.Scheme == "https")
+                {
+                    var client = new HttpClient();
+                    using(var stream = await client.GetStreamAsync(uri))
+                    {
+                        await stream.CopyToAsync(mem, token).ConfigureAwait(false);
+                        mem.Position = 0;
+                    }
+                }
+            }
+            catch (Exception)
+            {
+                return null;
+            }
+            return mem;
+        }
+
+        static HashAlgorithm GetHashAlgorithm(DocumentHashAlgorithm algorithm)
+        {
+            switch (algorithm)
+            {
+                case DocumentHashAlgorithm.SHA1:
+                    return SHA1.Create();
+                case DocumentHashAlgorithm.SHA256:
+                    return SHA256.Create();
+                case DocumentHashAlgorithm.MD5:
+                    return MD5.Create();
+            }
+            return null;
+        }
+
+        bool CheckPdbHash(byte[] computedHash)
+        {
+            if (computedHash.Length != doc.Hash.Length)
+                return false;
+
+            for (var i = 0; i < computedHash.Length; i++)
+                if (computedHash[i] != doc.Hash[i])
+                    return false;
+
+            return true;
+        }
+
+        byte[] ComputePdbHash(Stream sourceStream)
+        {
+            var algorithm = GetHashAlgorithm(doc.HashAlgorithm);
+            if (algorithm != null)
+                using(algorithm)
+            return algorithm.ComputeHash(sourceStream);
+
+            return Array.Empty<byte>();
+        }
+
+        public async Task<Stream> GetSourceAsync(bool checkHash, CancellationToken token = default(CancellationToken))
+        {
+            if (doc.EmbeddedSource.Length > 0)
+                return new MemoryStream(doc.EmbeddedSource, false);
+
+            foreach (var url in new [] { SourceUri, SourceLinkUri })
+            {
+                var mem = await GetDataAsync(url, token).ConfigureAwait(false);
+                if (mem != null && (!checkHash || CheckPdbHash(ComputePdbHash(mem))))
+                {
+                    mem.Position = 0;
+                    return mem;
+                }
+            }
+
+            return MemoryStream.Null;
+        }
+
+        public object ToScriptSource(int executionContextId, object executionContextAuxData)
+        {
+            return new
+            {
+                scriptId = SourceId.ToString(),
+                    url = Url,
+                    executionContextId,
+                    executionContextAuxData,
+                    //hash:  should be the v8 hash algo, managed implementation is pending
+                    dotNetUrl = DotNetUrl,
+            };
+        }
+    }
+
+    internal class DebugStore
+    {
+        List<AssemblyInfo> assemblies = new List<AssemblyInfo>();
+        readonly HttpClient client;
+        readonly ILogger logger;
+
+        public DebugStore(ILogger logger, HttpClient client)
+        {
+            this.client = client;
+            this.logger = logger;
+        }
+
+        public DebugStore(ILogger logger) : this(logger, new HttpClient())
+        { }
+
+        class DebugItem
+        {
+            public string Url { get; set; }
+            public Task<byte[][]> Data { get; set; }
+        }
+
+        public async IAsyncEnumerable<SourceFile> Load(SessionId sessionId, string[] loaded_files, [EnumeratorCancellation] CancellationToken token)
+        {
+            static bool MatchPdb(string asm, string pdb) => Path.ChangeExtension(asm, "pdb") == pdb;
+
+            var asm_files = new List<string>();
+            var pdb_files = new List<string>();
+            foreach (var file_name in loaded_files)
+            {
+                if (file_name.EndsWith(".pdb", StringComparison.OrdinalIgnoreCase))
+                    pdb_files.Add(file_name);
+                else
+                    asm_files.Add(file_name);
+            }
+
+            List<DebugItem> steps = new List<DebugItem>();
+            foreach (var url in asm_files)
+            {
+                try
+                {
+                    var pdb = pdb_files.FirstOrDefault(n => MatchPdb(url, n));
+                    steps.Add(
+                        new DebugItem
+                        {
+                            Url = url,
+                                Data = Task.WhenAll(client.GetByteArrayAsync(url), pdb != null ? client.GetByteArrayAsync(pdb) : Task.FromResult<byte[]>(null))
+                        });
+                }
+                catch (Exception e)
+                {
+                    logger.LogDebug($"Failed to read {url} ({e.Message})");
+                }
+            }
+
+            var resolver = new DefaultAssemblyResolver();
+            foreach (var step in steps)
+            {
+                AssemblyInfo assembly = null;
+                try
+                {
+                    var bytes = await step.Data.ConfigureAwait(false);
+                    assembly = new AssemblyInfo(resolver, step.Url, bytes[0], bytes[1]);
+                }
+                catch (Exception e)
+                {
+                    logger.LogDebug($"Failed to load {step.Url} ({e.Message})");
+                }
+                if (assembly == null)
+                    continue;
+
+                assemblies.Add(assembly);
+                foreach (var source in assembly.Sources)
+                    yield return source;
+            }
+        }
+
+        public IEnumerable<SourceFile> AllSources() => assemblies.SelectMany(a => a.Sources);
+
+        public SourceFile GetFileById(SourceId id) => AllSources().SingleOrDefault(f => f.SourceId.Equals(id));
+
+        public AssemblyInfo GetAssemblyByName(string name) => assemblies.FirstOrDefault(a => a.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
+
+        /*
+        V8 uses zero based indexing for both line and column.
+        PPDBs uses one based indexing for both line and column.
+        */
+        static bool Match(SequencePoint sp, SourceLocation start, SourceLocation end)
+        {
+            var spStart = (Line: sp.StartLine - 1, Column: sp.StartColumn - 1);
+            var spEnd = (Line: sp.EndLine - 1, Column: sp.EndColumn - 1);
+
+            if (start.Line > spEnd.Line)
+                return false;
+
+            if (start.Column > spEnd.Column && start.Line == spEnd.Line)
+                return false;
+
+            if (end.Line < spStart.Line)
+                return false;
+
+            if (end.Column < spStart.Column && end.Line == spStart.Line)
+                return false;
+
+            return true;
+        }
+
+        public List<SourceLocation> FindPossibleBreakpoints(SourceLocation start, SourceLocation end)
+        {
+            //XXX FIXME no idea what todo with locations on different files
+            if (start.Id != end.Id)
+            {
+                logger.LogDebug($"FindPossibleBreakpoints: documents differ (start: {start.Id}) (end {end.Id}");
+                return null;
+            }
+
+            var sourceId = start.Id;
+
+            var doc = GetFileById(sourceId);
+
+            var res = new List<SourceLocation>();
+            if (doc == null)
+            {
+                logger.LogDebug($"Could not find document {sourceId}");
+                return res;
+            }
+
+            foreach (var method in doc.Methods)
+            {
+                foreach (var sequencePoint in method.DebugInformation.SequencePoints)
+                {
+                    if (!sequencePoint.IsHidden && Match(sequencePoint, start, end))
+                        res.Add(new SourceLocation(method, sequencePoint));
+                }
+            }
+            return res;
+        }
+
+        /*
+        V8 uses zero based indexing for both line and column.
+        PPDBs uses one based indexing for both line and column.
+        */
+        static bool Match(SequencePoint sp, int line, int column)
+        {
+            var bp = (line: line + 1, column: column + 1);
+
+            if (sp.StartLine > bp.line || sp.EndLine < bp.line)
+                return false;
+
+            //Chrome sends a zero column even if getPossibleBreakpoints say something else
+            if (column == 0)
+                return true;
+
+            if (sp.StartColumn > bp.column && sp.StartLine == bp.line)
+                return false;
+
+            if (sp.EndColumn < bp.column && sp.EndLine == bp.line)
+                return false;
+
+            return true;
+        }
+
+        public IEnumerable<SourceLocation> FindBreakpointLocations(BreakpointRequest request)
+        {
+            request.TryResolve(this);
+
+            var asm = assemblies.FirstOrDefault(a => a.Name.Equals(request.Assembly, StringComparison.OrdinalIgnoreCase));
+            var sourceFile = asm?.Sources?.SingleOrDefault(s => s.DebuggerFileName.Equals(request.File, StringComparison.OrdinalIgnoreCase));
+
+            if (sourceFile == null)
+                yield break;
+
+            foreach (var method in sourceFile.Methods)
+            {
+                foreach (var sequencePoint in method.DebugInformation.SequencePoints)
+                {
+                    if (!sequencePoint.IsHidden && Match(sequencePoint, request.Line, request.Column))
+                        yield return new SourceLocation(method, sequencePoint);
+                }
+            }
+        }
+
+        public string ToUrl(SourceLocation location) => location != null ? GetFileById(location.Id).Url : "";
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs
new file mode 100644 (file)
index 0000000..3594111
--- /dev/null
@@ -0,0 +1,29 @@
+// 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.Net.WebSockets;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+    // This type is the public entrypoint that allows external code to attach the debugger proxy
+    // to a given websocket listener. Everything else in this package can be internal.
+
+    public class DebuggerProxy
+    {
+        private readonly MonoProxy proxy;
+
+        public DebuggerProxy(ILoggerFactory loggerFactory)
+        {
+            proxy = new MonoProxy(loggerFactory);
+        }
+
+        public Task Run(Uri browserUri, WebSocket ideSocket)
+        {
+            return proxy.Run(browserUri, ideSocket);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs
new file mode 100644 (file)
index 0000000..45edb96
--- /dev/null
@@ -0,0 +1,304 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+    public struct SessionId
+    {
+        public readonly string sessionId;
+
+        public SessionId(string sessionId)
+        {
+            this.sessionId = sessionId;
+        }
+
+        // hashset treats 0 as unset
+        public override int GetHashCode() => sessionId?.GetHashCode() ?? -1;
+
+        public override bool Equals(object obj) => (obj is SessionId) ? ((SessionId) obj).sessionId == sessionId : false;
+
+        public static bool operator ==(SessionId a, SessionId b) => a.sessionId == b.sessionId;
+
+        public static bool operator !=(SessionId a, SessionId b) => a.sessionId != b.sessionId;
+
+        public static SessionId Null { get; } = new SessionId();
+
+        public override string ToString() => $"session-{sessionId}";
+    }
+
+    public struct MessageId
+    {
+        public readonly string sessionId;
+        public readonly int id;
+
+        public MessageId(string sessionId, int id)
+        {
+            this.sessionId = sessionId;
+            this.id = id;
+        }
+
+        public static implicit operator SessionId(MessageId id) => new SessionId(id.sessionId);
+
+        public override string ToString() => $"msg-{sessionId}:::{id}";
+
+        public override int GetHashCode() => (sessionId?.GetHashCode() ?? 0) ^ id.GetHashCode();
+
+        public override bool Equals(object obj) => (obj is MessageId) ? ((MessageId) obj).sessionId == sessionId && ((MessageId) obj).id == id : false;
+    }
+
+    internal class DotnetObjectId
+    {
+        public string Scheme { get; }
+        public string Value { get; }
+
+        public static bool TryParse(JToken jToken, out DotnetObjectId objectId) => TryParse(jToken?.Value<string>(), out objectId);
+
+        public static bool TryParse(string id, out DotnetObjectId objectId)
+        {
+            objectId = null;
+            if (id == null)
+                return false;
+
+            if (!id.StartsWith("dotnet:"))
+                return false;
+
+            var parts = id.Split(":", 3);
+
+            if (parts.Length < 3)
+                return false;
+
+            objectId = new DotnetObjectId(parts[1], parts[2]);
+
+            return true;
+        }
+
+        public DotnetObjectId(string scheme, string value)
+        {
+            Scheme = scheme;
+            Value = value;
+        }
+
+        public override string ToString() => $"dotnet:{Scheme}:{Value}";
+    }
+
+    public struct Result
+    {
+        public JObject Value { get; private set; }
+        public JObject Error { get; private set; }
+
+        public bool IsOk => Value != null;
+        public bool IsErr => Error != null;
+
+        Result(JObject result, JObject error)
+        {
+            if (result != null && error != null)
+                throw new ArgumentException($"Both {nameof(result)} and {nameof(error)} arguments cannot be non-null.");
+
+            bool resultHasError = String.Compare((result?["result"] as JObject) ? ["subtype"]?.Value<string>(), "error") == 0;
+            if (result != null && resultHasError)
+            {
+                this.Value = null;
+                this.Error = result;
+            }
+            else
+            {
+                this.Value = result;
+                this.Error = error;
+            }
+        }
+
+        public static Result FromJson(JObject obj)
+        {
+            //Log ("protocol", $"from result: {obj}");
+            return new Result(obj["result"] as JObject, obj["error"] as JObject);
+        }
+
+        public static Result Ok(JObject ok) => new Result(ok, null);
+
+        public static Result OkFromObject(object ok) => Ok(JObject.FromObject(ok));
+
+        public static Result Err(JObject err) => new Result(null, err);
+
+        public static Result Err(string msg) => new Result(null, JObject.FromObject(new { message = msg }));
+
+        public static Result Exception(Exception e) => new Result(null, JObject.FromObject(new { message = e.Message }));
+
+        public JObject ToJObject(MessageId target)
+        {
+            if (IsOk)
+            {
+                return JObject.FromObject(new
+                {
+                    target.id,
+                        target.sessionId,
+                        result = Value
+                });
+            }
+            else
+            {
+                return JObject.FromObject(new
+                {
+                    target.id,
+                        target.sessionId,
+                        error = Error
+                });
+            }
+        }
+
+        public override string ToString()
+        {
+            return $"[Result: IsOk: {IsOk}, IsErr: {IsErr}, Value: {Value?.ToString ()}, Error: {Error?.ToString ()} ]";
+        }
+    }
+
+    internal class MonoCommands
+    {
+        public string expression { get; set; }
+        public string objectGroup { get; set; } = "mono-debugger";
+        public bool includeCommandLineAPI { get; set; } = false;
+        public bool silent { get; set; } = false;
+        public bool returnByValue { get; set; } = true;
+
+        public MonoCommands(string expression) => this.expression = expression;
+
+        public static MonoCommands GetCallStack() => new MonoCommands("MONO.mono_wasm_get_call_stack()");
+
+        public static MonoCommands IsRuntimeReady() => new MonoCommands("MONO.mono_wasm_runtime_is_ready");
+
+        public static MonoCommands StartSingleStepping(StepKind kind) => new MonoCommands($"MONO.mono_wasm_start_single_stepping ({(int)kind})");
+
+        public static MonoCommands GetLoadedFiles() => new MonoCommands("MONO.mono_wasm_get_loaded_files()");
+
+        public static MonoCommands ClearAllBreakpoints() => new MonoCommands("MONO.mono_wasm_clear_all_breakpoints()");
+
+        public static MonoCommands GetDetails(DotnetObjectId objectId, JToken args = null) => new MonoCommands($"MONO.mono_wasm_get_details ('{objectId}', {(args ?? "{ }")})");
+
+        public static MonoCommands GetScopeVariables(int scopeId, params VarInfo[] vars)
+        {
+            var var_ids = vars.Select(v => new { index = v.Index, name = v.Name }).ToArray();
+            return new MonoCommands($"MONO.mono_wasm_get_variables({scopeId}, {JsonConvert.SerializeObject (var_ids)})");
+        }
+
+        public static MonoCommands SetBreakpoint(string assemblyName, uint methodToken, int ilOffset) => new MonoCommands($"MONO.mono_wasm_set_breakpoint (\"{assemblyName}\", {methodToken}, {ilOffset})");
+
+        public static MonoCommands RemoveBreakpoint(int breakpointId) => new MonoCommands($"MONO.mono_wasm_remove_breakpoint({breakpointId})");
+
+        public static MonoCommands ReleaseObject(DotnetObjectId objectId) => new MonoCommands($"MONO.mono_wasm_release_object('{objectId}')");
+
+        public static MonoCommands CallFunctionOn(JToken args) => new MonoCommands($"MONO.mono_wasm_call_function_on ({args.ToString ()})");
+
+        public static MonoCommands Resume() => new MonoCommands($"MONO.mono_wasm_debugger_resume ()");
+    }
+
+    internal enum MonoErrorCodes
+    {
+        BpNotFound = 100000,
+    }
+
+    internal class MonoConstants
+    {
+        public const string RUNTIME_IS_READY = "mono_wasm_runtime_ready";
+    }
+
+    class Frame
+    {
+        public Frame(MethodInfo method, SourceLocation location, int id)
+        {
+            this.Method = method;
+            this.Location = location;
+            this.Id = id;
+        }
+
+        public MethodInfo Method { get; private set; }
+        public SourceLocation Location { get; private set; }
+        public int Id { get; private set; }
+    }
+
+    class Breakpoint
+    {
+        public SourceLocation Location { get; private set; }
+        public int RemoteId { get; set; }
+        public BreakpointState State { get; set; }
+        public string StackId { get; private set; }
+
+        public static bool TryParseId(string stackId, out int id)
+        {
+            id = -1;
+            if (stackId?.StartsWith("dotnet:", StringComparison.Ordinal) != true)
+                return false;
+
+            return int.TryParse(stackId.Substring("dotnet:".Length), out id);
+        }
+
+        public Breakpoint(string stackId, SourceLocation loc, BreakpointState state)
+        {
+            this.StackId = stackId;
+            this.Location = loc;
+            this.State = state;
+        }
+    }
+
+    enum BreakpointState
+    {
+        Active,
+        Disabled,
+        Pending
+    }
+
+    enum StepKind
+    {
+        Into,
+        Out,
+        Over
+    }
+
+    internal class ExecutionContext
+    {
+        public string DebuggerId { get; set; }
+        public Dictionary<string, BreakpointRequest> BreakpointRequests { get; } = new Dictionary<string, BreakpointRequest>();
+
+        public TaskCompletionSource<DebugStore> ready = null;
+        public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted;
+
+        public int Id { get; set; }
+        public object AuxData { get; set; }
+
+        public List<Frame> CallStack { get; set; }
+
+        public string[] LoadedFiles { get; set; }
+        internal DebugStore store;
+        public TaskCompletionSource<DebugStore> Source { get; } = new TaskCompletionSource<DebugStore>();
+
+        public Dictionary<string, JToken> LocalsCache = new Dictionary<string, JToken>();
+
+        public DebugStore Store
+        {
+            get
+            {
+                if (store == null || !Source.Task.IsCompleted)
+                    return null;
+
+                return store;
+            }
+        }
+
+        public void ClearState()
+        {
+            CallStack = null;
+            LocalsCache.Clear();
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs
new file mode 100644 (file)
index 0000000..656bd91
--- /dev/null
@@ -0,0 +1,381 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+    class DevToolsQueue
+    {
+        Task current_send;
+        List<byte[]> pending;
+
+        public WebSocket Ws { get; private set; }
+        public Task CurrentSend { get { return current_send; } }
+        public DevToolsQueue(WebSocket sock)
+        {
+            this.Ws = sock;
+            pending = new List<byte[]>();
+        }
+
+        public Task Send(byte[] bytes, CancellationToken token)
+        {
+            pending.Add(bytes);
+            if (pending.Count == 1)
+            {
+                if (current_send != null)
+                    throw new Exception("current_send MUST BE NULL IF THERE'S no pending send");
+                //logger.LogTrace ("sending {0} bytes", bytes.Length);
+                current_send = Ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, token);
+                return current_send;
+            }
+            return null;
+        }
+
+        public Task Pump(CancellationToken token)
+        {
+            current_send = null;
+            pending.RemoveAt(0);
+
+            if (pending.Count > 0)
+            {
+                if (current_send != null)
+                    throw new Exception("current_send MUST BE NULL IF THERE'S no pending send");
+
+                current_send = Ws.SendAsync(new ArraySegment<byte>(pending[0]), WebSocketMessageType.Text, true, token);
+                return current_send;
+            }
+            return null;
+        }
+    }
+
+    internal class DevToolsProxy
+    {
+        TaskCompletionSource<bool> side_exception = new TaskCompletionSource<bool>();
+        TaskCompletionSource<bool> client_initiated_close = new TaskCompletionSource<bool>();
+        Dictionary<MessageId, TaskCompletionSource<Result>> pending_cmds = new Dictionary<MessageId, TaskCompletionSource<Result>>();
+        ClientWebSocket browser;
+        WebSocket ide;
+        int next_cmd_id;
+        List<Task> pending_ops = new List<Task>();
+        List<DevToolsQueue> queues = new List<DevToolsQueue>();
+
+        protected readonly ILogger logger;
+
+        public DevToolsProxy(ILoggerFactory loggerFactory)
+        {
+            logger = loggerFactory.CreateLogger<DevToolsProxy>();
+        }
+
+        protected virtual Task<bool> AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+        {
+            return Task.FromResult(false);
+        }
+
+        protected virtual Task<bool> AcceptCommand(MessageId id, string method, JObject args, CancellationToken token)
+        {
+            return Task.FromResult(false);
+        }
+
+        async Task<string> ReadOne(WebSocket socket, CancellationToken token)
+        {
+            byte[] buff = new byte[4000];
+            var mem = new MemoryStream();
+            while (true)
+            {
+
+                if (socket.State != WebSocketState.Open)
+                {
+                    Log("error", $"DevToolsProxy: Socket is no longer open.");
+                    client_initiated_close.TrySetResult(true);
+                    return null;
+                }
+
+                var result = await socket.ReceiveAsync(new ArraySegment<byte>(buff), token);
+                if (result.MessageType == WebSocketMessageType.Close)
+                {
+                    client_initiated_close.TrySetResult(true);
+                    return null;
+                }
+
+                mem.Write(buff, 0, result.Count);
+
+                if (result.EndOfMessage)
+                    return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int) mem.Length);
+            }
+        }
+
+        DevToolsQueue GetQueueForSocket(WebSocket ws)
+        {
+            return queues.FirstOrDefault(q => q.Ws == ws);
+        }
+
+        DevToolsQueue GetQueueForTask(Task task)
+        {
+            return queues.FirstOrDefault(q => q.CurrentSend == task);
+        }
+
+        void Send(WebSocket to, JObject o, CancellationToken token)
+        {
+            var sender = browser == to ? "Send-browser" : "Send-ide";
+
+            var method = o["method"]?.ToString();
+            //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
+            Log("protocol", $"{sender}: " + JsonConvert.SerializeObject(o));
+            var bytes = Encoding.UTF8.GetBytes(o.ToString());
+
+            var queue = GetQueueForSocket(to);
+
+            var task = queue.Send(bytes, token);
+            if (task != null)
+                pending_ops.Add(task);
+        }
+
+        async Task OnEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+        {
+            try
+            {
+                if (!await AcceptEvent(sessionId, method, args, token))
+                {
+                    //logger.LogDebug ("proxy browser: {0}::{1}",method, args);
+                    SendEventInternal(sessionId, method, args, token);
+                }
+            }
+            catch (Exception e)
+            {
+                side_exception.TrySetException(e);
+            }
+        }
+
+        async Task OnCommand(MessageId id, string method, JObject args, CancellationToken token)
+        {
+            try
+            {
+                if (!await AcceptCommand(id, method, args, token))
+                {
+                    var res = await SendCommandInternal(id, method, args, token);
+                    SendResponseInternal(id, res, token);
+                }
+            }
+            catch (Exception e)
+            {
+                side_exception.TrySetException(e);
+            }
+        }
+
+        void OnResponse(MessageId id, Result result)
+        {
+            //logger.LogTrace ("got id {0} res {1}", id, result);
+            // Fixme
+            if (pending_cmds.Remove(id, out var task))
+            {
+                task.SetResult(result);
+                return;
+            }
+            logger.LogError("Cannot respond to command: {id} with result: {result} - command is not pending", id, result);
+        }
+
+        void ProcessBrowserMessage(string msg, CancellationToken token)
+        {
+            var res = JObject.Parse(msg);
+
+            var method = res["method"]?.ToString();
+            //if (method != "Debugger.scriptParsed" && method != "Runtime.consoleAPICalled")
+            Log("protocol", $"browser: {msg}");
+
+            if (res["id"] == null)
+                pending_ops.Add(OnEvent(new SessionId(res["sessionId"]?.Value<string>()), res["method"].Value<string>(), res["params"] as JObject, token));
+            else
+                OnResponse(new MessageId(res["sessionId"]?.Value<string>(), res["id"].Value<int>()), Result.FromJson(res));
+        }
+
+        void ProcessIdeMessage(string msg, CancellationToken token)
+        {
+            Log("protocol", $"ide: {msg}");
+            if (!string.IsNullOrEmpty(msg))
+            {
+                var res = JObject.Parse(msg);
+                pending_ops.Add(OnCommand(
+                    new MessageId(res["sessionId"]?.Value<string>(), res["id"].Value<int>()),
+                    res["method"].Value<string>(),
+                    res["params"] as JObject, token));
+            }
+        }
+
+        internal async Task<Result> SendCommand(SessionId id, string method, JObject args, CancellationToken token)
+        {
+            //Log ("verbose", $"sending command {method}: {args}");
+            return await SendCommandInternal(id, method, args, token);
+        }
+
+        Task<Result> SendCommandInternal(SessionId sessionId, string method, JObject args, CancellationToken token)
+        {
+            int id = Interlocked.Increment(ref next_cmd_id);
+
+            var o = JObject.FromObject(new
+            {
+                id,
+                method,
+                @params = args
+            });
+            if (sessionId.sessionId != null)
+                o["sessionId"] = sessionId.sessionId;
+            var tcs = new TaskCompletionSource<Result>();
+
+            var msgId = new MessageId(sessionId.sessionId, id);
+            //Log ("verbose", $"add cmd id {sessionId}-{id}");
+            pending_cmds[msgId] = tcs;
+
+            Send(this.browser, o, token);
+            return tcs.Task;
+        }
+
+        public void SendEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+        {
+            //Log ("verbose", $"sending event {method}: {args}");
+            SendEventInternal(sessionId, method, args, token);
+        }
+
+        void SendEventInternal(SessionId sessionId, string method, JObject args, CancellationToken token)
+        {
+            var o = JObject.FromObject(new
+            {
+                method,
+                @params = args
+            });
+            if (sessionId.sessionId != null)
+                o["sessionId"] = sessionId.sessionId;
+
+            Send(this.ide, o, token);
+        }
+
+        internal void SendResponse(MessageId id, Result result, CancellationToken token)
+        {
+            SendResponseInternal(id, result, token);
+        }
+
+        void SendResponseInternal(MessageId id, Result result, CancellationToken token)
+        {
+            JObject o = result.ToJObject(id);
+            if (result.IsErr)
+                logger.LogError($"sending error response for id: {id} -> {result}");
+
+            Send(this.ide, o, token);
+        }
+
+        // , HttpContext context)
+        public async Task Run(Uri browserUri, WebSocket ideSocket)
+        {
+            Log("info", $"DevToolsProxy: Starting on {browserUri}");
+            using(this.ide = ideSocket)
+            {
+                Log("verbose", $"DevToolsProxy: IDE waiting for connection on {browserUri}");
+                queues.Add(new DevToolsQueue(this.ide));
+                using(this.browser = new ClientWebSocket())
+                {
+                    this.browser.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
+                    await this.browser.ConnectAsync(browserUri, CancellationToken.None);
+                    queues.Add(new DevToolsQueue(this.browser));
+
+                    Log("verbose", $"DevToolsProxy: Client connected on {browserUri}");
+                    var x = new CancellationTokenSource();
+
+                    pending_ops.Add(ReadOne(browser, x.Token));
+                    pending_ops.Add(ReadOne(ide, x.Token));
+                    pending_ops.Add(side_exception.Task);
+                    pending_ops.Add(client_initiated_close.Task);
+
+                    try
+                    {
+                        while (!x.IsCancellationRequested)
+                        {
+                            var task = await Task.WhenAny(pending_ops.ToArray());
+                            //logger.LogTrace ("pump {0} {1}", task, pending_ops.IndexOf (task));
+                            if (task == pending_ops[0])
+                            {
+                                var msg = ((Task<string>) task).Result;
+                                if (msg != null)
+                                {
+                                    pending_ops[0] = ReadOne(browser, x.Token); //queue next read
+                                    ProcessBrowserMessage(msg, x.Token);
+                                }
+                            }
+                            else if (task == pending_ops[1])
+                            {
+                                var msg = ((Task<string>) task).Result;
+                                if (msg != null)
+                                {
+                                    pending_ops[1] = ReadOne(ide, x.Token); //queue next read
+                                    ProcessIdeMessage(msg, x.Token);
+                                }
+                            }
+                            else if (task == pending_ops[2])
+                            {
+                                var res = ((Task<bool>) task).Result;
+                                throw new Exception("side task must always complete with an exception, what's going on???");
+                            }
+                            else if (task == pending_ops[3])
+                            {
+                                var res = ((Task<bool>) task).Result;
+                                Log("verbose", $"DevToolsProxy: Client initiated close from {browserUri}");
+                                x.Cancel();
+                            }
+                            else
+                            {
+                                //must be a background task
+                                pending_ops.Remove(task);
+                                var queue = GetQueueForTask(task);
+                                if (queue != null)
+                                {
+                                    var tsk = queue.Pump(x.Token);
+                                    if (tsk != null)
+                                        pending_ops.Add(tsk);
+                                }
+                            }
+                        }
+                    }
+                    catch (Exception e)
+                    {
+                        Log("error", $"DevToolsProxy::Run: Exception {e}");
+                        //throw;
+                    }
+                    finally
+                    {
+                        if (!x.IsCancellationRequested)
+                            x.Cancel();
+                    }
+                }
+            }
+        }
+
+        protected void Log(string priority, string msg)
+        {
+            switch (priority)
+            {
+                case "protocol":
+                    logger.LogTrace(msg);
+                    break;
+                case "verbose":
+                    logger.LogDebug(msg);
+                    break;
+                case "info":
+                case "warning":
+                case "error":
+                default:
+                    logger.LogDebug(msg);
+                    break;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs
new file mode 100644 (file)
index 0000000..982fa87
--- /dev/null
@@ -0,0 +1,213 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Emit;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+    internal class EvaluateExpression
+    {
+
+        class FindThisExpression : CSharpSyntaxWalker
+        {
+            public List<string> thisExpressions = new List<string>();
+            public SyntaxTree syntaxTree;
+            public FindThisExpression(SyntaxTree syntax)
+            {
+                syntaxTree = syntax;
+            }
+            public override void Visit(SyntaxNode node)
+            {
+                if (node is ThisExpressionSyntax)
+                {
+                    if (node.Parent is MemberAccessExpressionSyntax thisParent && thisParent.Name is IdentifierNameSyntax)
+                    {
+                        IdentifierNameSyntax var = thisParent.Name as IdentifierNameSyntax;
+                        thisExpressions.Add(var.Identifier.Text);
+                        var newRoot = syntaxTree.GetRoot().ReplaceNode(node.Parent, thisParent.Name);
+                        syntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);
+                        this.Visit(GetExpressionFromSyntaxTree(syntaxTree));
+                    }
+                }
+                else
+                    base.Visit(node);
+            }
+
+            public async Task CheckIfIsProperty(MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
+            {
+                foreach (var var in thisExpressions)
+                {
+                    JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var, true, token);
+                    if (value == null)
+                        throw new Exception($"The property {var} does not exist in the current context");
+                }
+            }
+        }
+
+        class FindVariableNMethodCall : CSharpSyntaxWalker
+        {
+            public List<IdentifierNameSyntax> variables = new List<IdentifierNameSyntax>();
+            public List<ThisExpressionSyntax> thisList = new List<ThisExpressionSyntax>();
+            public List<InvocationExpressionSyntax> methodCall = new List<InvocationExpressionSyntax>();
+            public List<object> values = new List<Object>();
+
+            public override void Visit(SyntaxNode node)
+            {
+                if (node is IdentifierNameSyntax identifier && !variables.Any(x => x.Identifier.Text == identifier.Identifier.Text))
+                    variables.Add(identifier);
+                if (node is InvocationExpressionSyntax)
+                {
+                    methodCall.Add(node as InvocationExpressionSyntax);
+                    throw new Exception("Method Call is not implemented yet");
+                }
+                if (node is AssignmentExpressionSyntax)
+                    throw new Exception("Assignment is not implemented yet");
+                base.Visit(node);
+            }
+            public async Task<SyntaxTree> ReplaceVars(SyntaxTree syntaxTree, MonoProxy proxy, MessageId msg_id, int scope_id, CancellationToken token)
+            {
+                CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot();
+                foreach (var var in variables)
+                {
+                    ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax;
+                    MethodDeclarationSyntax method = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
+
+                    JToken value = await proxy.TryGetVariableValue(msg_id, scope_id, var.Identifier.Text, false, token);
+
+                    if (value == null)
+                        throw new Exception($"The name {var.Identifier.Text} does not exist in the current context");
+
+                    values.Add(ConvertJSToCSharpType(value["value"]));
+
+                    var updatedMethod = method.AddParameterListParameters(
+                        SyntaxFactory.Parameter(
+                            SyntaxFactory.Identifier(var.Identifier.Text))
+                        .WithType(SyntaxFactory.ParseTypeName(GetTypeFullName(value["value"]))));
+                    root = root.ReplaceNode(method, updatedMethod);
+                }
+                syntaxTree = syntaxTree.WithRootAndOptions(root, syntaxTree.Options);
+                return syntaxTree;
+            }
+
+            private object ConvertJSToCSharpType(JToken variable)
+            {
+                var value = variable["value"];
+                var type = variable["type"].Value<string>();
+                var subType = variable["subtype"]?.Value<string>();
+
+                switch (type)
+                {
+                    case "string":
+                        return value?.Value<string>();
+                    case "number":
+                        return value?.Value<double>();
+                    case "boolean":
+                        return value?.Value<bool>();
+                    case "object":
+                        if (subType == "null")
+                            return null;
+                        break;
+                }
+                throw new Exception($"Evaluate of this datatype {type} not implemented yet");
+            }
+
+            private string GetTypeFullName(JToken variable)
+            {
+                var type = variable["type"].ToString();
+                var subType = variable["subtype"]?.Value<string>();
+                object value = ConvertJSToCSharpType(variable);
+
+                switch (type)
+                {
+                    case "object":
+                        {
+                            if (subType == "null")
+                                return variable["className"].Value<string>();
+                            break;
+                        }
+                    default:
+                        return value.GetType().FullName;
+                }
+                throw new Exception($"Evaluate of this datatype {type} not implemented yet");
+            }
+        }
+
+        static SyntaxNode GetExpressionFromSyntaxTree(SyntaxTree syntaxTree)
+        {
+            CompilationUnitSyntax root = syntaxTree.GetCompilationUnitRoot();
+            ClassDeclarationSyntax classDeclaration = root.Members.ElementAt(0) as ClassDeclarationSyntax;
+            MethodDeclarationSyntax methodDeclaration = classDeclaration.Members.ElementAt(0) as MethodDeclarationSyntax;
+            BlockSyntax blockValue = methodDeclaration.Body;
+            ReturnStatementSyntax returnValue = blockValue.Statements.ElementAt(0) as ReturnStatementSyntax;
+            InvocationExpressionSyntax expressionInvocation = returnValue.Expression as InvocationExpressionSyntax;
+            MemberAccessExpressionSyntax expressionMember = expressionInvocation.Expression as MemberAccessExpressionSyntax;
+            ParenthesizedExpressionSyntax expressionParenthesized = expressionMember.Expression as ParenthesizedExpressionSyntax;
+            return expressionParenthesized.Expression;
+        }
+
+        internal static async Task<string> CompileAndRunTheExpression(MonoProxy proxy, MessageId msg_id, int scope_id, string expression, CancellationToken token)
+        {
+            FindVariableNMethodCall findVarNMethodCall = new FindVariableNMethodCall();
+            string retString;
+            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(@"
+                               using System;
+                               public class CompileAndRunTheExpression
+                               {
+                                       public string Evaluate()
+                                       {
+                                               return (" + expression + @").ToString(); 
+                                       }
+                               }");
+
+            FindThisExpression findThisExpression = new FindThisExpression(syntaxTree);
+            var expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
+            findThisExpression.Visit(expressionTree);
+            await findThisExpression.CheckIfIsProperty(proxy, msg_id, scope_id, token);
+            syntaxTree = findThisExpression.syntaxTree;
+
+            expressionTree = GetExpressionFromSyntaxTree(syntaxTree);
+            findVarNMethodCall.Visit(expressionTree);
+
+            syntaxTree = await findVarNMethodCall.ReplaceVars(syntaxTree, proxy, msg_id, scope_id, token);
+
+            MetadataReference[] references = new MetadataReference[]
+            {
+                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
+                MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location)
+            };
+
+            CSharpCompilation compilation = CSharpCompilation.Create(
+                "compileAndRunTheExpression",
+                syntaxTrees : new [] { syntaxTree },
+                references : references,
+                options : new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
+            using(var ms = new MemoryStream())
+            {
+                EmitResult result = compilation.Emit(ms);
+                ms.Seek(0, SeekOrigin.Begin);
+                Assembly assembly = Assembly.Load(ms.ToArray());
+                Type type = assembly.GetType("CompileAndRunTheExpression");
+                object obj = Activator.CreateInstance(type);
+                var ret = type.InvokeMember("Evaluate",
+                    BindingFlags.Default | BindingFlags.InvokeMethod,
+                    null,
+                    obj,
+                    findVarNMethodCall.values.ToArray());
+                retString = ret.ToString();
+            }
+            return retString;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs
new file mode 100644 (file)
index 0000000..197d499
--- /dev/null
@@ -0,0 +1,998 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+
+    internal class MonoProxy : DevToolsProxy
+    {
+        HashSet<SessionId> sessions = new HashSet<SessionId>();
+        Dictionary<SessionId, ExecutionContext> contexts = new Dictionary<SessionId, ExecutionContext>();
+
+        public MonoProxy(ILoggerFactory loggerFactory, bool hideWebDriver = true) : base(loggerFactory) { hideWebDriver = true; }
+
+        readonly bool hideWebDriver;
+
+        internal ExecutionContext GetContext(SessionId sessionId)
+        {
+            if (contexts.TryGetValue(sessionId, out var context))
+                return context;
+
+            throw new ArgumentException($"Invalid Session: \"{sessionId}\"", nameof(sessionId));
+        }
+
+        bool UpdateContext(SessionId sessionId, ExecutionContext executionContext, out ExecutionContext previousExecutionContext)
+        {
+            var previous = contexts.TryGetValue(sessionId, out previousExecutionContext);
+            contexts[sessionId] = executionContext;
+            return previous;
+        }
+
+        internal Task<Result> SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token);
+
+        protected override async Task<bool> AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token)
+        {
+            switch (method)
+            {
+                case "Runtime.consoleAPICalled":
+                    {
+                        var type = args["type"]?.ToString();
+                        if (type == "debug")
+                        {
+                            var a = args["args"];
+                            if (a?[0] ? ["value"]?.ToString() == MonoConstants.RUNTIME_IS_READY &&
+                                a?[1] ? ["value"]?.ToString() == "fe00e07a-5519-4dfe-b35a-f867dbaf2e28")
+                            {
+                                if (a.Count() > 2)
+                                {
+                                    try
+                                    {
+                                        // The optional 3rd argument is the stringified assembly
+                                        // list so that we don't have to make more round trips
+                                        var context = GetContext(sessionId);
+                                        var loaded = a?[2] ? ["value"]?.ToString();
+                                        if (loaded != null)
+                                            context.LoadedFiles = JToken.Parse(loaded).ToObject<string[]>();
+                                    }
+                                    catch (InvalidCastException ice)
+                                    {
+                                        Log("verbose", ice.ToString());
+                                    }
+                                }
+                                await RuntimeReady(sessionId, token);
+                            }
+
+                        }
+                        break;
+                    }
+
+                case "Runtime.executionContextCreated":
+                    {
+                        SendEvent(sessionId, method, args, token);
+                        var ctx = args?["context"];
+                        var aux_data = ctx?["auxData"] as JObject;
+                        var id = ctx["id"].Value<int>();
+                        if (aux_data != null)
+                        {
+                            var is_default = aux_data["isDefault"]?.Value<bool>();
+                            if (is_default == true)
+                            {
+                                await OnDefaultContext(sessionId, new ExecutionContext { Id = id, AuxData = aux_data }, token);
+                            }
+                        }
+                        return true;
+                    }
+
+                case "Debugger.paused":
+                    {
+                        //TODO figure out how to stich out more frames and, in particular what happens when real wasm is on the stack
+                        var top_func = args?["callFrames"] ? [0] ? ["functionName"]?.Value<string>();
+
+                        if (top_func == "mono_wasm_fire_bp" || top_func == "_mono_wasm_fire_bp")
+                        {
+                            return await OnBreakpointHit(sessionId, args, token);
+                        }
+                        break;
+                    }
+
+                case "Debugger.breakpointResolved":
+                    {
+                        break;
+                    }
+
+                case "Debugger.scriptParsed":
+                    {
+                        var url = args?["url"]?.Value<string>() ?? "";
+
+                        switch (url)
+                        {
+                            case var _ when url == "":
+                                case var _ when url.StartsWith("wasm://", StringComparison.Ordinal):
+                                {
+                                    Log("verbose", $"ignoring wasm: Debugger.scriptParsed {url}");
+                                    return true;
+                                }
+                        }
+                        Log("verbose", $"proxying Debugger.scriptParsed ({sessionId.sessionId}) {url} {args}");
+                        break;
+                    }
+
+                case "Target.attachedToTarget":
+                    {
+                        if (args["targetInfo"]["type"]?.ToString() == "page")
+                            await DeleteWebDriver(new SessionId(args["sessionId"]?.ToString()), token);
+                        break;
+                    }
+
+            }
+
+            return false;
+        }
+
+        async Task<bool> IsRuntimeAlreadyReadyAlready(SessionId sessionId, CancellationToken token)
+        {
+            if (contexts.TryGetValue(sessionId, out var context) && context.IsRuntimeReady)
+                return true;
+
+            var res = await SendMonoCommand(sessionId, MonoCommands.IsRuntimeReady(), token);
+            return res.Value?["result"] ? ["value"]?.Value<bool>() ?? false;
+        }
+
+        static int bpIdGenerator;
+
+        protected override async Task<bool> AcceptCommand(MessageId id, string method, JObject args, CancellationToken token)
+        {
+            // Inspector doesn't use the Target domain or sessions
+            // so we try to init immediately
+            if (hideWebDriver && id == SessionId.Null)
+                await DeleteWebDriver(id, token);
+
+            if (!contexts.TryGetValue(id, out var context))
+                return false;
+
+            switch (method)
+            {
+                case "Target.attachToTarget":
+                    {
+                        var resp = await SendCommand(id, method, args, token);
+                        await DeleteWebDriver(new SessionId(resp.Value["sessionId"]?.ToString()), token);
+                        break;
+                    }
+
+                case "Debugger.enable":
+                    {
+                        System.Console.WriteLine("recebi o Debugger.enable");
+                        var resp = await SendCommand(id, method, args, token);
+
+                        context.DebuggerId = resp.Value["debuggerId"]?.ToString();
+
+                        if (await IsRuntimeAlreadyReadyAlready(id, token))
+                            await RuntimeReady(id, token);
+
+                        SendResponse(id, resp, token);
+                        return true;
+                    }
+
+                case "Debugger.getScriptSource":
+                    {
+                        var script = args?["scriptId"]?.Value<string>();
+                        return await OnGetScriptSource(id, script, token);
+                    }
+
+                case "Runtime.compileScript":
+                    {
+                        var exp = args?["expression"]?.Value<string>();
+                        if (exp.StartsWith("//dotnet:", StringComparison.Ordinal))
+                        {
+                            OnCompileDotnetScript(id, token);
+                            return true;
+                        }
+                        break;
+                    }
+
+                case "Debugger.getPossibleBreakpoints":
+                    {
+                        var resp = await SendCommand(id, method, args, token);
+                        if (resp.IsOk && resp.Value["locations"].HasValues)
+                        {
+                            SendResponse(id, resp, token);
+                            return true;
+                        }
+
+                        var start = SourceLocation.Parse(args?["start"] as JObject);
+                        //FIXME support variant where restrictToFunction=true and end is omitted
+                        var end = SourceLocation.Parse(args?["end"] as JObject);
+                        if (start != null && end != null && await GetPossibleBreakpoints(id, start, end, token))
+                            return true;
+
+                        SendResponse(id, resp, token);
+                        return true;
+                    }
+
+                case "Debugger.setBreakpoint":
+                    {
+                        break;
+                    }
+
+                case "Debugger.setBreakpointByUrl":
+                    {
+                        var resp = await SendCommand(id, method, args, token);
+                        if (!resp.IsOk)
+                        {
+                            SendResponse(id, resp, token);
+                            return true;
+                        }
+
+                        var bpid = resp.Value["breakpointId"]?.ToString();
+                        var locations = resp.Value["locations"]?.Values<object>();
+                        var request = BreakpointRequest.Parse(bpid, args);
+
+                        // is the store done loading?
+                        var loaded = context.Source.Task.IsCompleted;
+                        if (!loaded)
+                        {
+                            // Send and empty response immediately if not
+                            // and register the breakpoint for resolution
+                            context.BreakpointRequests[bpid] = request;
+                            SendResponse(id, resp, token);
+                        }
+
+                        if (await IsRuntimeAlreadyReadyAlready(id, token))
+                        {
+                            var store = await RuntimeReady(id, token);
+
+                            Log("verbose", $"BP req {args}");
+                            await SetBreakpoint(id, store, request, !loaded, token);
+                        }
+
+                        if (loaded)
+                        {
+                            // we were already loaded so we should send a response
+                            // with the locations included and register the request
+                            context.BreakpointRequests[bpid] = request;
+                            var result = Result.OkFromObject(request.AsSetBreakpointByUrlResponse(locations));
+                            SendResponse(id, result, token);
+
+                        }
+                        return true;
+                    }
+
+                case "Debugger.removeBreakpoint":
+                    {
+                        await RemoveBreakpoint(id, args, token);
+                        break;
+                    }
+
+                case "Debugger.resume":
+                    {
+                        await OnResume(id, token);
+                        break;
+                    }
+
+                case "Debugger.stepInto":
+                    {
+                        return await Step(id, StepKind.Into, token);
+                    }
+
+                case "Debugger.stepOut":
+                    {
+                        return await Step(id, StepKind.Out, token);
+                    }
+
+                case "Debugger.stepOver":
+                    {
+                        return await Step(id, StepKind.Over, token);
+                    }
+
+                case "Debugger.evaluateOnCallFrame":
+                    {
+                        if (!DotnetObjectId.TryParse(args?["callFrameId"], out var objectId))
+                            return false;
+
+                        switch (objectId.Scheme)
+                        {
+                            case "scope":
+                                return await OnEvaluateOnCallFrame(id,
+                                    int.Parse(objectId.Value),
+                                    args?["expression"]?.Value<string>(), token);
+                            default:
+                                return false;
+                        }
+                    }
+
+                case "Runtime.getProperties":
+                    {
+                        if (!DotnetObjectId.TryParse(args?["objectId"], out var objectId))
+                            break;
+
+                        var result = await RuntimeGetProperties(id, objectId, args, token);
+                        SendResponse(id, result, token);
+                        return true;
+                    }
+
+                case "Runtime.releaseObject":
+                    {
+                        if (!(DotnetObjectId.TryParse(args["objectId"], out var objectId) && objectId.Scheme == "cfo_res"))
+                            break;
+
+                        await SendMonoCommand(id, MonoCommands.ReleaseObject(objectId), token);
+                        SendResponse(id, Result.OkFromObject(new { }), token);
+                        return true;
+                    }
+
+                    // Protocol extensions
+                case "DotnetDebugger.getMethodLocation":
+                    {
+                        Console.WriteLine("set-breakpoint-by-method: " + id + " " + args);
+
+                        var store = await RuntimeReady(id, token);
+                        string aname = args["assemblyName"]?.Value<string>();
+                        string typeName = args["typeName"]?.Value<string>();
+                        string methodName = args["methodName"]?.Value<string>();
+                        if (aname == null || typeName == null || methodName == null)
+                        {
+                            SendResponse(id, Result.Err("Invalid protocol message '" + args + "'."), token);
+                            return true;
+                        }
+
+                        // GetAssemblyByName seems to work on file names
+                        var assembly = store.GetAssemblyByName(aname);
+                        if (assembly == null)
+                            assembly = store.GetAssemblyByName(aname + ".exe");
+                        if (assembly == null)
+                            assembly = store.GetAssemblyByName(aname + ".dll");
+                        if (assembly == null)
+                        {
+                            SendResponse(id, Result.Err("Assembly '" + aname + "' not found."), token);
+                            return true;
+                        }
+
+                        var type = assembly.GetTypeByName(typeName);
+                        if (type == null)
+                        {
+                            SendResponse(id, Result.Err($"Type '{typeName}' not found."), token);
+                            return true;
+                        }
+
+                        var methodInfo = type.Methods.FirstOrDefault(m => m.Name == methodName);
+                        if (methodInfo == null)
+                        {
+                            // Maybe this is an async method, in which case the debug info is attached
+                            // to the async method implementation, in class named:
+                            //      `{type_name}/<method_name>::MoveNext`
+                            methodInfo = assembly.TypesByName.Values.SingleOrDefault(t => t.FullName.StartsWith($"{typeName}/<{methodName}>")) ?
+                                .Methods.FirstOrDefault(mi => mi.Name == "MoveNext");
+                        }
+
+                        if (methodInfo == null)
+                        {
+                            SendResponse(id, Result.Err($"Method '{typeName}:{methodName}' not found."), token);
+                            return true;
+                        }
+
+                        var src_url = methodInfo.Assembly.Sources.Single(sf => sf.SourceId == methodInfo.SourceId).Url;
+                        SendResponse(id, Result.OkFromObject(new
+                        {
+                            result = new { line = methodInfo.StartLocation.Line, column = methodInfo.StartLocation.Column, url = src_url }
+                        }), token);
+
+                        return true;
+                    }
+                case "Runtime.callFunctionOn":
+                    {
+                        if (!DotnetObjectId.TryParse(args["objectId"], out var objectId))
+                            return false;
+
+                        if (objectId.Scheme == "scope")
+                        {
+                            SendResponse(id,
+                                Result.Exception(new ArgumentException(
+                                    $"Runtime.callFunctionOn not supported with scope ({objectId}).")),
+                                token);
+                            return true;
+                        }
+
+                        var res = await SendMonoCommand(id, MonoCommands.CallFunctionOn(args), token);
+                        var res_value_type = res.Value?["result"] ? ["value"]?.Type;
+
+                        if (res.IsOk && res_value_type == JTokenType.Object || res_value_type == JTokenType.Object)
+                            res = Result.OkFromObject(new { result = res.Value["result"]["value"] });
+
+                        SendResponse(id, res, token);
+                        return true;
+                    }
+            }
+
+            return false;
+        }
+
+        async Task<Result> RuntimeGetProperties(MessageId id, DotnetObjectId objectId, JToken args, CancellationToken token)
+        {
+            if (objectId.Scheme == "scope")
+                return await GetScopeProperties(id, int.Parse(objectId.Value), token);
+
+            var res = await SendMonoCommand(id, MonoCommands.GetDetails(objectId, args), token);
+            if (res.IsErr)
+                return res;
+
+            if (objectId.Scheme == "cfo_res")
+            {
+                // Runtime.callFunctionOn result object
+                var value_json_str = res.Value["result"] ? ["value"] ? ["__value_as_json_string__"]?.Value<string>();
+                if (value_json_str != null)
+                {
+                    res = Result.OkFromObject(new
+                    {
+                        result = JArray.Parse(value_json_str)
+                    });
+                }
+                else
+                {
+                    res = Result.OkFromObject(new { result = new { } });
+                }
+            }
+            else
+            {
+                res = Result.Ok(JObject.FromObject(new { result = res.Value["result"]["value"] }));
+            }
+
+            return res;
+        }
+
+        //static int frame_id=0;
+        async Task<bool> OnBreakpointHit(SessionId sessionId, JObject args, CancellationToken token)
+        {
+            //FIXME we should send release objects every now and then? Or intercept those we inject and deal in the runtime
+            var res = await SendMonoCommand(sessionId, MonoCommands.GetCallStack(), token);
+            var orig_callframes = args?["callFrames"]?.Values<JObject>();
+            var context = GetContext(sessionId);
+
+            if (res.IsErr)
+            {
+                //Give up and send the original call stack
+                return false;
+            }
+
+            //step one, figure out where did we hit
+            var res_value = res.Value?["result"] ? ["value"];
+            if (res_value == null || res_value is JValue)
+            {
+                //Give up and send the original call stack
+                return false;
+            }
+
+            Log("verbose", $"call stack (err is {res.Error} value is:\n{res.Value}");
+            var bp_id = res_value?["breakpoint_id"]?.Value<int>();
+            Log("verbose", $"We just hit bp {bp_id}");
+            if (!bp_id.HasValue)
+            {
+                //Give up and send the original call stack
+                return false;
+            }
+
+            var bp = context.BreakpointRequests.Values.SelectMany(v => v.Locations).FirstOrDefault(b => b.RemoteId == bp_id.Value);
+
+            var callFrames = new List<object>();
+            foreach (var frame in orig_callframes)
+            {
+                var function_name = frame["functionName"]?.Value<string>();
+                var url = frame["url"]?.Value<string>();
+                if ("mono_wasm_fire_bp" == function_name ||"_mono_wasm_fire_bp" == function_name)
+                {
+                    var frames = new List<Frame>();
+                    int frame_id = 0;
+                    var the_mono_frames = res.Value?["result"] ? ["value"] ? ["frames"]?.Values<JObject>();
+
+                    foreach (var mono_frame in the_mono_frames)
+                    {
+                        ++frame_id;
+                        var il_pos = mono_frame["il_pos"].Value<int>();
+                        var method_token = mono_frame["method_token"].Value<uint>();
+                        var assembly_name = mono_frame["assembly_name"].Value<string>();
+
+                        // This can be different than `method.Name`, like in case of generic methods
+                        var method_name = mono_frame["method_name"]?.Value<string>();
+
+                        var store = await LoadStore(sessionId, token);
+                        var asm = store.GetAssemblyByName(assembly_name);
+                        if (asm == null)
+                        {
+                            Log("info", $"Unable to find assembly: {assembly_name}");
+                            continue;
+                        }
+
+                        var method = asm.GetMethodByToken(method_token);
+
+                        if (method == null)
+                        {
+                            Log("info", $"Unable to find il offset: {il_pos} in method token: {method_token} assembly name: {assembly_name}");
+                            continue;
+                        }
+
+                        var location = method?.GetLocationByIl(il_pos);
+
+                        // When hitting a breakpoint on the "IncrementCount" method in the standard
+                        // Blazor project template, one of the stack frames is inside mscorlib.dll
+                        // and we get location==null for it. It will trigger a NullReferenceException
+                        // if we don't skip over that stack frame.
+                        if (location == null)
+                        {
+                            continue;
+                        }
+
+                        Log("info", $"frame il offset: {il_pos} method token: {method_token} assembly name: {assembly_name}");
+                        Log("info", $"\tmethod {method_name} location: {location}");
+                        frames.Add(new Frame(method, location, frame_id - 1));
+
+                        callFrames.Add(new
+                        {
+                            functionName = method_name,
+                                callFrameId = $"dotnet:scope:{frame_id-1}",
+                                functionLocation = method.StartLocation.AsLocation(),
+
+                                location = location.AsLocation(),
+
+                                url = store.ToUrl(location),
+
+                                scopeChain = new []
+                                {
+                                    new
+                                    {
+                                        type = "local",
+                                            @object = new
+                                            {
+                                                @type = "object",
+                                                    className = "Object",
+                                                    description = "Object",
+                                                    objectId = $"dotnet:scope:{frame_id-1}",
+                                            },
+                                            name = method_name,
+                                            startLocation = method.StartLocation.AsLocation(),
+                                            endLocation = method.EndLocation.AsLocation(),
+                                    }
+                                }
+                        });
+
+                        context.CallStack = frames;
+
+                    }
+                }
+                else if (!(function_name.StartsWith("wasm-function", StringComparison.Ordinal) ||
+                        url.StartsWith("wasm://wasm/", StringComparison.Ordinal)))
+                {
+                    callFrames.Add(frame);
+                }
+            }
+
+            var bp_list = new string[bp == null ? 0 : 1];
+            if (bp != null)
+                bp_list[0] = bp.StackId;
+
+            var o = JObject.FromObject(new
+            {
+                callFrames,
+                reason = "other", //other means breakpoint
+                hitBreakpoints = bp_list,
+            });
+
+            SendEvent(sessionId, "Debugger.paused", o, token);
+            return true;
+        }
+
+        async Task OnDefaultContext(SessionId sessionId, ExecutionContext context, CancellationToken token)
+        {
+            Log("verbose", "Default context created, clearing state and sending events");
+            if (UpdateContext(sessionId, context, out var previousContext))
+            {
+                foreach (var kvp in previousContext.BreakpointRequests)
+                {
+                    context.BreakpointRequests[kvp.Key] = kvp.Value.Clone();
+                }
+            }
+
+            if (await IsRuntimeAlreadyReadyAlready(sessionId, token))
+                await RuntimeReady(sessionId, token);
+        }
+
+        async Task OnResume(MessageId msg_id, CancellationToken token)
+        {
+            var ctx = GetContext(msg_id);
+            if (ctx.CallStack != null)
+            {
+                // Stopped on managed code
+                await SendMonoCommand(msg_id, MonoCommands.Resume(), token);
+            }
+
+            //discard managed frames
+            GetContext(msg_id).ClearState();
+        }
+
+        async Task<bool> Step(MessageId msg_id, StepKind kind, CancellationToken token)
+        {
+            var context = GetContext(msg_id);
+            if (context.CallStack == null)
+                return false;
+
+            if (context.CallStack.Count <= 1 && kind == StepKind.Out)
+                return false;
+
+            var res = await SendMonoCommand(msg_id, MonoCommands.StartSingleStepping(kind), token);
+
+            var ret_code = res.Value?["result"] ? ["value"]?.Value<int>();
+
+            if (ret_code.HasValue && ret_code.Value == 0)
+            {
+                context.ClearState();
+                await SendCommand(msg_id, "Debugger.stepOut", new JObject(), token);
+                return false;
+            }
+
+            SendResponse(msg_id, Result.Ok(new JObject()), token);
+
+            context.ClearState();
+
+            await SendCommand(msg_id, "Debugger.resume", new JObject(), token);
+            return true;
+        }
+
+        internal bool TryFindVariableValueInCache(ExecutionContext ctx, string expression, bool only_search_on_this, out JToken obj)
+        {
+            if (ctx.LocalsCache.TryGetValue(expression, out obj))
+            {
+                if (only_search_on_this && obj["fromThis"] == null)
+                    return false;
+                return true;
+            }
+            return false;
+        }
+
+        internal async Task<JToken> TryGetVariableValue(MessageId msg_id, int scope_id, string expression, bool only_search_on_this, CancellationToken token)
+        {
+            JToken thisValue = null;
+            var context = GetContext(msg_id);
+            if (context.CallStack == null)
+                return null;
+
+            if (TryFindVariableValueInCache(context, expression, only_search_on_this, out JToken obj))
+                return obj;
+
+            var scope = context.CallStack.FirstOrDefault(s => s.Id == scope_id);
+            var live_vars = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
+            //get_this
+            var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, live_vars), token);
+
+            var scope_values = res.Value?["result"] ? ["value"]?.Values<JObject>()?.ToArray();
+            thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value<string>() == "this");
+
+            if (!only_search_on_this)
+            {
+                if (thisValue != null && expression == "this")
+                    return thisValue;
+
+                var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value<string>() == expression);
+                if (value != null)
+                    return value;
+            }
+
+            //search in scope
+            if (thisValue != null)
+            {
+                if (!DotnetObjectId.TryParse(thisValue["value"]["objectId"], out var objectId))
+                    return null;
+
+                res = await SendMonoCommand(msg_id, MonoCommands.GetDetails(objectId), token);
+                scope_values = res.Value?["result"] ? ["value"]?.Values<JObject>().ToArray();
+                var foundValue = scope_values.FirstOrDefault(v => v["name"].Value<string>() == expression);
+                if (foundValue != null)
+                {
+                    foundValue["fromThis"] = true;
+                    context.LocalsCache[foundValue["name"].Value<string>()] = foundValue;
+                    return foundValue;
+                }
+            }
+            return null;
+        }
+
+        async Task<bool> OnEvaluateOnCallFrame(MessageId msg_id, int scope_id, string expression, CancellationToken token)
+        {
+            try
+            {
+                var context = GetContext(msg_id);
+                if (context.CallStack == null)
+                    return false;
+
+                var varValue = await TryGetVariableValue(msg_id, scope_id, expression, false, token);
+
+                if (varValue != null)
+                {
+                    SendResponse(msg_id, Result.OkFromObject(new
+                    {
+                        result = varValue["value"]
+                    }), token);
+                    return true;
+                }
+
+                string retValue = await EvaluateExpression.CompileAndRunTheExpression(this, msg_id, scope_id, expression, token);
+                SendResponse(msg_id, Result.OkFromObject(new
+                {
+                    result = new
+                    {
+                        value = retValue
+                    }
+                }), token);
+                return true;
+            }
+            catch (Exception e)
+            {
+                logger.LogDebug(e, $"Error in EvaluateOnCallFrame for expression '{expression}.");
+            }
+            return false;
+        }
+
+        async Task<Result> GetScopeProperties(MessageId msg_id, int scope_id, CancellationToken token)
+        {
+            try
+            {
+                var ctx = GetContext(msg_id);
+                var scope = ctx.CallStack.FirstOrDefault(s => s.Id == scope_id);
+                if (scope == null)
+                    return Result.Err(JObject.FromObject(new { message = $"Could not find scope with id #{scope_id}" }));
+
+                var var_ids = scope.Method.GetLiveVarsAt(scope.Location.CliLocation.Offset);
+                var res = await SendMonoCommand(msg_id, MonoCommands.GetScopeVariables(scope.Id, var_ids), token);
+
+                //if we fail we just buble that to the IDE (and let it panic over it)
+                if (res.IsErr)
+                    return res;
+
+                var values = res.Value?["result"] ? ["value"]?.Values<JObject>().ToArray();
+
+                if (values == null || values.Length == 0)
+                    return Result.OkFromObject(new { result = Array.Empty<object>() });
+
+                foreach (var value in values)
+                    ctx.LocalsCache[value["name"]?.Value<string>()] = value;
+
+                return Result.OkFromObject(new { result = values });
+            }
+            catch (Exception exception)
+            {
+                Log("verbose", $"Error resolving scope properties {exception.Message}");
+                return Result.Exception(exception);
+            }
+        }
+
+        async Task<Breakpoint> SetMonoBreakpoint(SessionId sessionId, string reqId, SourceLocation location, CancellationToken token)
+        {
+            var bp = new Breakpoint(reqId, location, BreakpointState.Pending);
+            var asm_name = bp.Location.CliLocation.Method.Assembly.Name;
+            var method_token = bp.Location.CliLocation.Method.Token;
+            var il_offset = bp.Location.CliLocation.Offset;
+
+            var res = await SendMonoCommand(sessionId, MonoCommands.SetBreakpoint(asm_name, method_token, il_offset), token);
+            var ret_code = res.Value?["result"] ? ["value"]?.Value<int>();
+
+            if (ret_code.HasValue)
+            {
+                bp.RemoteId = ret_code.Value;
+                bp.State = BreakpointState.Active;
+                //Log ("verbose", $"BP local id {bp.LocalId} enabled with remote id {bp.RemoteId}");
+            }
+
+            return bp;
+        }
+
+        async Task<DebugStore> LoadStore(SessionId sessionId, CancellationToken token)
+        {
+            var context = GetContext(sessionId);
+
+            if (Interlocked.CompareExchange(ref context.store, new DebugStore(logger), null) != null)
+                return await context.Source.Task;
+
+            try
+            {
+                var loaded_files = context.LoadedFiles;
+
+                if (loaded_files == null)
+                {
+                    var loaded = await SendMonoCommand(sessionId, MonoCommands.GetLoadedFiles(), token);
+                    loaded_files = loaded.Value?["result"] ? ["value"]?.ToObject<string[]>();
+                }
+
+                await
+                foreach (var source in context.store.Load(sessionId, loaded_files, token).WithCancellation(token))
+                {
+                    var scriptSource = JObject.FromObject(source.ToScriptSource(context.Id, context.AuxData));
+                    Log("verbose", $"\tsending {source.Url} {context.Id} {sessionId.sessionId}");
+
+                    SendEvent(sessionId, "Debugger.scriptParsed", scriptSource, token);
+
+                    foreach (var req in context.BreakpointRequests.Values)
+                    {
+                        if (req.TryResolve(source))
+                        {
+                            await SetBreakpoint(sessionId, context.store, req, true, token);
+                        }
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                context.Source.SetException(e);
+            }
+
+            if (!context.Source.Task.IsCompleted)
+                context.Source.SetResult(context.store);
+            return context.store;
+        }
+
+        async Task<DebugStore> RuntimeReady(SessionId sessionId, CancellationToken token)
+        {
+            var context = GetContext(sessionId);
+            if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource<DebugStore>(), null) != null)
+                return await context.ready.Task;
+
+            var clear_result = await SendMonoCommand(sessionId, MonoCommands.ClearAllBreakpoints(), token);
+            if (clear_result.IsErr)
+            {
+                Log("verbose", $"Failed to clear breakpoints due to {clear_result}");
+            }
+
+            var store = await LoadStore(sessionId, token);
+
+            context.ready.SetResult(store);
+            SendEvent(sessionId, "Mono.runtimeReady", new JObject(), token);
+            return store;
+        }
+
+        async Task RemoveBreakpoint(MessageId msg_id, JObject args, CancellationToken token)
+        {
+            var bpid = args?["breakpointId"]?.Value<string>();
+
+            var context = GetContext(msg_id);
+            if (!context.BreakpointRequests.TryGetValue(bpid, out var breakpointRequest))
+                return;
+
+            foreach (var bp in breakpointRequest.Locations)
+            {
+                var res = await SendMonoCommand(msg_id, MonoCommands.RemoveBreakpoint(bp.RemoteId), token);
+                var ret_code = res.Value?["result"] ? ["value"]?.Value<int>();
+
+                if (ret_code.HasValue)
+                {
+                    bp.RemoteId = -1;
+                    bp.State = BreakpointState.Disabled;
+                }
+            }
+            breakpointRequest.Locations.Clear();
+        }
+
+        async Task SetBreakpoint(SessionId sessionId, DebugStore store, BreakpointRequest req, bool sendResolvedEvent, CancellationToken token)
+        {
+            var context = GetContext(sessionId);
+            if (req.Locations.Any())
+            {
+                Log("debug", $"locations already loaded for {req.Id}");
+                return;
+            }
+
+            var comparer = new SourceLocation.LocationComparer();
+            // if column is specified the frontend wants the exact matches
+            // and will clear the bp if it isn't close enoug
+            var locations = store.FindBreakpointLocations(req)
+                .Distinct(comparer)
+                .Where(l => l.Line == req.Line && (req.Column == 0 || l.Column == req.Column))
+                .OrderBy(l => l.Column)
+                .GroupBy(l => l.Id);
+
+            logger.LogDebug("BP request for '{req}' runtime ready {context.RuntimeReady}", req, GetContext(sessionId).IsRuntimeReady);
+
+            var breakpoints = new List<Breakpoint>();
+
+            foreach (var sourceId in locations)
+            {
+                var loc = sourceId.First();
+                var bp = await SetMonoBreakpoint(sessionId, req.Id, loc, token);
+
+                // If we didn't successfully enable the breakpoint
+                // don't add it to the list of locations for this id
+                if (bp.State != BreakpointState.Active)
+                    continue;
+
+                breakpoints.Add(bp);
+
+                var resolvedLocation = new
+                {
+                    breakpointId = req.Id,
+                    location = loc.AsLocation()
+                };
+
+                if (sendResolvedEvent)
+                    SendEvent(sessionId, "Debugger.breakpointResolved", JObject.FromObject(resolvedLocation), token);
+            }
+
+            req.Locations.AddRange(breakpoints);
+            return;
+        }
+
+        async Task<bool> GetPossibleBreakpoints(MessageId msg, SourceLocation start, SourceLocation end, CancellationToken token)
+        {
+            var bps = (await RuntimeReady(msg, token)).FindPossibleBreakpoints(start, end);
+
+            if (bps == null)
+                return false;
+
+            var response = new { locations = bps.Select(b => b.AsLocation()) };
+
+            SendResponse(msg, Result.OkFromObject(response), token);
+            return true;
+        }
+
+        void OnCompileDotnetScript(MessageId msg_id, CancellationToken token)
+        {
+            SendResponse(msg_id, Result.OkFromObject(new { }), token);
+        }
+
+        async Task<bool> OnGetScriptSource(MessageId msg_id, string script_id, CancellationToken token)
+        {
+            if (!SourceId.TryParse(script_id, out var id))
+                return false;
+
+            var src_file = (await LoadStore(msg_id, token)).GetFileById(id);
+
+            try
+            {
+                var uri = new Uri(src_file.Url);
+                string source = $"// Unable to find document {src_file.SourceUri}";
+
+                using(var data = await src_file.GetSourceAsync(checkHash: false, token: token))
+                {
+                    if (data.Length == 0)
+                        return false;
+
+                    using(var reader = new StreamReader(data))
+                    source = await reader.ReadToEndAsync();
+                }
+                SendResponse(msg_id, Result.OkFromObject(new { scriptSource = source }), token);
+            }
+            catch (Exception e)
+            {
+                var o = new
+                {
+                    scriptSource = $"// Unable to read document ({e.Message})\n" +
+                    $"Local path: {src_file?.SourceUri}\n" +
+                    $"SourceLink path: {src_file?.SourceLinkUri}\n"
+                };
+
+                SendResponse(msg_id, Result.OkFromObject(o), token);
+            }
+            return true;
+        }
+
+        async Task DeleteWebDriver(SessionId sessionId, CancellationToken token)
+        {
+            // see https://github.com/mono/mono/issues/19549 for background
+            if (hideWebDriver && sessions.Add(sessionId))
+            {
+                var res = await SendCommand(sessionId,
+                    "Page.addScriptToEvaluateOnNewDocument",
+                    JObject.FromObject(new { source = "delete navigator.constructor.prototype.webdriver" }),
+                    token);
+
+                if (sessionId != SessionId.Null && !res.IsOk)
+                    sessions.Remove(sessionId);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs
new file mode 100644 (file)
index 0000000..022e41c
--- /dev/null
@@ -0,0 +1,605 @@
+// 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 Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+    public class ArrayTests : DebuggerTestBase
+    {
+
+        [Theory]
+        [InlineData(19, 8, "PrimitiveTypeLocals", false, 0, false)]
+        [InlineData(19, 8, "PrimitiveTypeLocals", false, 0, true)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+        public async Task InspectPrimitiveTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+            line, col,
+            entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:PrimitiveTypeLocals",
+            method_name : method_name,
+            etype_name: "int",
+            local_var_name_prefix: "int",
+            array : new [] { TNumber(4), TNumber(70), TNumber(1) },
+            array_elements : null,
+            test_prev_frame : test_prev_frame,
+            frame_idx : frame_idx,
+            use_cfo : use_cfo);
+
+        [Theory]
+        [InlineData(36, 8, "ValueTypeLocals", false, 0, false)]
+        [InlineData(36, 8, "ValueTypeLocals", false, 0, true)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+        public async Task InspectValueTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+            line, col,
+            entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:ValueTypeLocals",
+            method_name : method_name,
+            etype_name: "DebuggerTests.Point",
+            local_var_name_prefix: "point",
+            array : new []
+            {
+                TValueType("DebuggerTests.Point"),
+                    TValueType("DebuggerTests.Point"),
+            },
+            array_elements : new []
+            {
+                TPoint(5, -2, "point_arr#Id#0", "Green"),
+                    TPoint(123, 0, "point_arr#Id#1", "Blue")
+            },
+            test_prev_frame : test_prev_frame,
+            frame_idx : frame_idx,
+            use_cfo : use_cfo);
+
+        [Theory]
+        [InlineData(54, 8, "ObjectTypeLocals", false, 0, false)]
+        [InlineData(54, 8, "ObjectTypeLocals", false, 0, true)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+        public async Task InspectObjectArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+            line, col,
+            entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:ObjectTypeLocals",
+            method_name : method_name,
+            etype_name: "DebuggerTests.SimpleClass",
+            local_var_name_prefix: "class",
+            array : new []
+            {
+                TObject("DebuggerTests.SimpleClass"),
+                    TObject("DebuggerTests.SimpleClass", is_null : true),
+                    TObject("DebuggerTests.SimpleClass")
+            },
+            array_elements : new []
+            {
+                TSimpleClass(5, -2, "class_arr#Id#0", "Green"),
+                    null, // Element is null
+                    TSimpleClass(123, 0, "class_arr#Id#2", "Blue")
+            },
+            test_prev_frame : test_prev_frame,
+            frame_idx : frame_idx,
+            use_cfo : use_cfo);
+
+        [Theory]
+        [InlineData(72, 8, "GenericTypeLocals", false, 0, false)]
+        [InlineData(72, 8, "GenericTypeLocals", false, 0, true)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+        public async Task InspectGenericTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+            line, col,
+            entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:GenericTypeLocals",
+            method_name : method_name,
+            etype_name: "DebuggerTests.GenericClass<int>",
+            local_var_name_prefix: "gclass",
+            array : new []
+            {
+                TObject("DebuggerTests.GenericClass<int>", is_null : true),
+                    TObject("DebuggerTests.GenericClass<int>"),
+                    TObject("DebuggerTests.GenericClass<int>")
+            },
+            array_elements : new []
+            {
+                null, // Element is null
+                new
+                {
+                    Id = TString("gclass_arr#1#Id"),
+                        Color = TEnum("DebuggerTests.RGB", "Red"),
+                        Value = TNumber(5)
+                },
+                new
+                {
+                    Id = TString("gclass_arr#2#Id"),
+                        Color = TEnum("DebuggerTests.RGB", "Blue"),
+                        Value = TNumber(-12)
+                }
+            },
+            test_prev_frame : test_prev_frame,
+            frame_idx : frame_idx,
+            use_cfo : use_cfo);
+
+        [Theory]
+        [InlineData(89, 8, "GenericValueTypeLocals", false, 0, false)]
+        [InlineData(89, 8, "GenericValueTypeLocals", false, 0, true)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+        public async Task InspectGenericValueTypeArrayLocals(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+            line, col,
+            entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:GenericValueTypeLocals",
+            method_name : method_name,
+            etype_name: "DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>",
+            local_var_name_prefix: "gvclass",
+            array : new []
+            {
+                TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
+                    TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>")
+            },
+            array_elements : new []
+            {
+                new
+                {
+                    Id = TString("gvclass_arr#1#Id"),
+                        Color = TEnum("DebuggerTests.RGB", "Red"),
+                        Value = TPoint(100, 200, "gvclass_arr#1#Value#Id", "Red")
+                },
+                new
+                {
+                    Id = TString("gvclass_arr#2#Id"),
+                        Color = TEnum("DebuggerTests.RGB", "Blue"),
+                        Value = TPoint(10, 20, "gvclass_arr#2#Value#Id", "Green")
+                }
+            },
+            test_prev_frame : test_prev_frame,
+            frame_idx : frame_idx,
+            use_cfo : use_cfo);
+
+        [Theory]
+        [InlineData(213, 8, "GenericValueTypeLocals2", false, 0, false)]
+        [InlineData(213, 8, "GenericValueTypeLocals2", false, 0, true)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, false)]
+        [InlineData(100, 8, "YetAnotherMethod", true, 2, true)]
+        public async Task InspectGenericValueTypeArrayLocals2(int line, int col, string method_name, bool test_prev_frame, int frame_idx, bool use_cfo) => await TestSimpleArrayLocals(
+            line, col,
+            entry_method_name: "[debugger-test] DebuggerTests.ArrayTestsClass:GenericValueTypeLocals2",
+            method_name : method_name,
+            etype_name: "DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>",
+            local_var_name_prefix: "gvclass",
+            array : new []
+            {
+                TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>"),
+                    TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point[]>")
+            },
+            array_elements : new []
+            {
+                new
+                {
+                    Id = TString("gvclass_arr#0#Id"),
+                        Color = TEnum("DebuggerTests.RGB", "Red"),
+                        Value = new []
+                        {
+                            TPoint(100, 200, "gvclass_arr#0#0#Value#Id", "Red"),
+                                TPoint(100, 200, "gvclass_arr#0#1#Value#Id", "Green")
+                        }
+                },
+                new
+                {
+                    Id = TString("gvclass_arr#1#Id"),
+                        Color = TEnum("DebuggerTests.RGB", "Blue"),
+                        Value = new []
+                        {
+                            TPoint(100, 200, "gvclass_arr#1#0#Value#Id", "Green"),
+                                TPoint(100, 200, "gvclass_arr#1#1#Value#Id", "Blue")
+                        }
+                }
+            },
+            test_prev_frame : test_prev_frame,
+            frame_idx : frame_idx,
+            use_cfo : use_cfo);
+
+        async Task TestSimpleArrayLocals(int line, int col, string entry_method_name, string method_name, string etype_name,
+            string local_var_name_prefix, object[] array, object[] array_elements,
+            bool test_prev_frame = false, int frame_idx = 0, bool use_cfo = false)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+                    $"'{entry_method_name}', { (test_prev_frame ? "true" : "false") }" +
+                    "); }, 1);";
+
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, method_name);
+
+                var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                Assert.Equal(4, locals.Count());
+                CheckArray(locals, $"{local_var_name_prefix}_arr", $"{etype_name}[]");
+                CheckArray(locals, $"{local_var_name_prefix}_arr_empty", $"{etype_name}[]");
+                CheckObject(locals, $"{local_var_name_prefix}_arr_null", $"{etype_name}[]", is_null : true);
+                CheckBool(locals, "call_other", test_prev_frame);
+
+                var local_arr_name = $"{local_var_name_prefix}_arr";
+
+                JToken prefix_arr;
+                if (use_cfo)
+                { // Use `Runtime.callFunctionOn` to get the properties
+                    var frame = pause_location["callFrames"][frame_idx];
+                    var name = local_arr_name;
+                    var fl = await GetProperties(frame["callFrameId"].Value<string>());
+                    var l_obj = GetAndAssertObjectWithName(locals, name);
+                    var l_objectId = l_obj["value"]["objectId"]?.Value<string>();
+
+                    Assert.True(!String.IsNullOrEmpty(l_objectId), $"No objectId found for {name}");
+
+                    prefix_arr = await GetObjectWithCFO(l_objectId);
+                }
+                else
+                {
+                    prefix_arr = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], local_arr_name);
+                }
+
+                await CheckProps(prefix_arr, array, local_arr_name);
+
+                if (array_elements?.Length > 0)
+                {
+                    for (int i = 0; i < array_elements.Length; i++)
+                    {
+                        var i_str = i.ToString();
+                        var label = $"{local_var_name_prefix}_arr[{i}]";
+                        if (array_elements[i] == null)
+                        {
+                            var act_i = prefix_arr.FirstOrDefault(jt => jt["name"]?.Value<string>() == i_str);
+                            Assert.True(act_i != null, $"[{label}] Couldn't find array element [{i_str}]");
+
+                            await CheckValue(act_i["value"], TObject(etype_name, is_null : true), label);
+                        }
+                        else
+                        {
+                            await CompareObjectPropertiesFor(prefix_arr, i_str, array_elements[i], label : label);
+                        }
+                    }
+                }
+
+                var props = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], $"{local_var_name_prefix}_arr_empty");
+                await CheckProps(props, new object[0], "${local_var_name_prefix}_arr_empty");
+            });
+
+            async Task<JToken> GetObjectWithCFO(string objectId, JObject fn_args = null)
+            {
+                var fn_decl = "function () { return this; }";
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = fn_decl,
+                        objectId = objectId
+                });
+
+                if (fn_args != null)
+                    cfo_args["arguments"] = fn_args;
+
+                // callFunctionOn
+                var result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+
+                return await GetProperties(result.Value["result"]["objectId"]?.Value<string>(), fn_args);
+            }
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectObjectArrayMembers(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+            int line = 227;
+            int col = 12;
+            string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:ObjectArrayMembers";
+            string method_name = "PlaceholderMethod";
+            int frame_idx = 1;
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() { invoke_static_method (" +
+                    $"'{entry_method_name}'" +
+                    "); }, 1);";
+
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, method_name);
+                var locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                Assert.Single(locals);
+                CheckObject(locals, "c", "DebuggerTests.Container");
+
+                var c_props = await GetObjectOnFrame(pause_location["callFrames"][frame_idx], "c");
+                await CheckProps(c_props, new
+                    {
+                        id = TString("c#id"),
+                            ClassArrayProperty = TArray("DebuggerTests.SimpleClass[]", 3),
+                            ClassArrayField = TArray("DebuggerTests.SimpleClass[]", 3),
+                            PointsProperty = TArray("DebuggerTests.Point[]", 2),
+                            PointsField = TArray("DebuggerTests.Point[]", 2)
+                    },
+                    "c"
+                );
+
+                await CompareObjectPropertiesFor(c_props, "ClassArrayProperty",
+                    new []
+                    {
+                        TSimpleClass(5, -2, "ClassArrayProperty#Id#0", "Green"),
+                            TSimpleClass(30, 1293, "ClassArrayProperty#Id#1", "Green"),
+                            TObject("DebuggerTests.SimpleClass", is_null : true)
+                    },
+                    label: "InspectLocalsWithStructsStaticAsync");
+
+                await CompareObjectPropertiesFor(c_props, "ClassArrayField",
+                    new []
+                    {
+                        TObject("DebuggerTests.SimpleClass", is_null : true),
+                            TSimpleClass(5, -2, "ClassArrayField#Id#1", "Blue"),
+                            TSimpleClass(30, 1293, "ClassArrayField#Id#2", "Green")
+                    },
+                    label: "c#ClassArrayField");
+
+                await CompareObjectPropertiesFor(c_props, "PointsProperty",
+                    new []
+                    {
+                        TPoint(5, -2, "PointsProperty#Id#0", "Green"),
+                            TPoint(123, 0, "PointsProperty#Id#1", "Blue"),
+                    },
+                    label: "c#PointsProperty");
+
+                await CompareObjectPropertiesFor(c_props, "PointsField",
+                    new []
+                    {
+                        TPoint(5, -2, "PointsField#Id#0", "Green"),
+                            TPoint(123, 0, "PointsField#Id#1", "Blue"),
+                    },
+                    label: "c#PointsField");
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectValueTypeArrayLocalsStaticAsync(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+            int line = 157;
+            int col = 12;
+            string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:ValueTypeLocalsAsync";
+            string method_name = "MoveNext"; // BUG: this should be ValueTypeLocalsAsync
+            int frame_idx = 0;
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+                    $"'{entry_method_name}', false" // *false* here keeps us only in the static method
+                    +
+                    "); }, 1);";
+
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, method_name);
+                var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    call_other = TBool(false),
+                        gvclass_arr = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", 2),
+                        gvclass_arr_empty = TArray("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]"),
+                        gvclass_arr_null = TObject("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>[]", is_null : true),
+                        gvclass = TValueType("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
+                        // BUG: this shouldn't be null!
+                        points = TObject("DebuggerTests.Point[]", is_null : true)
+                }, "ValueTypeLocalsAsync#locals");
+
+                var local_var_name_prefix = "gvclass";
+                await CompareObjectPropertiesFor(frame_locals, local_var_name_prefix, new
+                {
+                    Id = TString(null),
+                        Color = TEnum("DebuggerTests.RGB", "Red"),
+                        Value = TPoint(0, 0, null, "Red")
+                });
+
+                await CompareObjectPropertiesFor(frame_locals, $"{local_var_name_prefix}_arr",
+                    new []
+                    {
+                        new
+                        {
+                            Id = TString("gvclass_arr#1#Id"),
+                                Color = TEnum("DebuggerTests.RGB", "Red"),
+                                Value = TPoint(100, 200, "gvclass_arr#1#Value#Id", "Red")
+                        },
+                        new
+                        {
+                            Id = TString("gvclass_arr#2#Id"),
+                                Color = TEnum("DebuggerTests.RGB", "Blue"),
+                                Value = TPoint(10, 20, "gvclass_arr#2#Value#Id", "Green")
+                        }
+                    }
+                );
+                await CompareObjectPropertiesFor(frame_locals, $"{local_var_name_prefix}_arr_empty",
+                    new object[0]);
+            });
+        }
+
+        // TODO: Check previous frame too
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectValueTypeArrayLocalsInstanceAsync(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+            int line = 170;
+            int col = 12;
+            string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:ValueTypeLocalsAsync";
+            int frame_idx = 0;
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+                    $"'{entry_method_name}', true" +
+                    "); }, 1);";
+
+                // BUG: Should be InspectValueTypeArrayLocalsInstanceAsync
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, "MoveNext");
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    t1 = TObject("DebuggerTests.SimpleGenericStruct<DebuggerTests.Point>"),
+                        @this = TObject("DebuggerTests.ArrayTestsClass"),
+                        point_arr = TArray("DebuggerTests.Point[]", 2),
+                        point = TValueType("DebuggerTests.Point")
+                }, "InspectValueTypeArrayLocalsInstanceAsync#locals");
+
+                await CompareObjectPropertiesFor(frame_locals, "t1",
+                    new
+                    {
+                        Id = TString("gvclass_arr#1#Id"),
+                            Color = TEnum("DebuggerTests.RGB", "Red"),
+                            Value = TPoint(100, 200, "gvclass_arr#1#Value#Id", "Red")
+                    });
+
+                await CompareObjectPropertiesFor(frame_locals, "point_arr",
+                    new []
+                    {
+                        TPoint(5, -2, "point_arr#Id#0", "Red"),
+                            TPoint(123, 0, "point_arr#Id#1", "Blue"),
+                    }
+                );
+
+                await CompareObjectPropertiesFor(frame_locals, "point",
+                    TPoint(45, 51, "point#Id", "Green"));
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectValueTypeArrayLocalsInAsyncStaticStructMethod(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+            int line = 244;
+            int col = 12;
+            string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod";
+            int frame_idx = 0;
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+                //await SetBreakpoint (debugger_test_loc, 143, 3);
+
+                var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+                    $"'{entry_method_name}', false" +
+                    "); }, 1);";
+
+                // BUG: Should be InspectValueTypeArrayLocalsInstanceAsync
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, "MoveNext");
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    call_other = TBool(false),
+                        local_i = TNumber(5),
+                        sc = TSimpleClass(10, 45, "sc#Id", "Blue")
+                }, "InspectValueTypeArrayLocalsInAsyncStaticStructMethod#locals");
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectValueTypeArrayLocalsInAsyncInstanceStructMethod(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+            int line = 251;
+            int col = 12;
+            string entry_method_name = "[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod";
+            int frame_idx = 0;
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() { invoke_static_method_async (" +
+                    $"'{entry_method_name}', true" +
+                    "); }, 1);";
+
+                // BUG: Should be InspectValueTypeArrayLocalsInstanceAsync
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, "MoveNext");
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                    {
+                        sc_arg = TObject("DebuggerTests.SimpleClass"),
+                            @this = TValueType("DebuggerTests.Point"),
+                            local_gs = TValueType("DebuggerTests.SimpleGenericStruct<int>")
+                    },
+                    "locals#0");
+
+                await CompareObjectPropertiesFor(frame_locals, "local_gs",
+                    new
+                    {
+                        Id = TString("local_gs#Id"),
+                            Color = TEnum("DebuggerTests.RGB", "Green"),
+                            Value = TNumber(4)
+                    },
+                    label: "local_gs#0");
+
+                await CompareObjectPropertiesFor(frame_locals, "sc_arg",
+                    TSimpleClass(10, 45, "sc_arg#Id", "Blue"),
+                    label: "sc_arg#0");
+
+                await CompareObjectPropertiesFor(frame_locals, "this",
+                    TPoint(90, -4, "point#Id", "Green"),
+                    label: "this#0");
+            });
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs
new file mode 100644 (file)
index 0000000..5c66bb2
--- /dev/null
@@ -0,0 +1,857 @@
+// 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 Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+    public class CallFunctionOnTests : DebuggerTestBase
+    {
+
+        // This tests `callFunctionOn` with a function that the vscode-js-debug extension uses
+        // Using this here as a non-trivial test case
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, 10, false)]
+        [InlineData("big_array_js_test (0);", "/other.js", 8, 1, 0, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 10, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 0);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 0, true)]
+        public async Task CheckVSCodeTestFunction1(string eval_fn, string bp_loc, int line, int col, int len, bool roundtrip)
+        {
+            string vscode_fn0 = "function(){const e={__proto__:this.__proto__},t=Object.getOwnPropertyNames(this);for(let r=0;r<t.length;++r){const n=t[r],i=n>>>0;if(String(i>>>0)===n&&i>>>0!=4294967295)continue;const a=Object.getOwnPropertyDescriptor(this,n);a&&Object.defineProperty(e,n,a)}return e}";
+
+            await RunCallFunctionOn(eval_fn, vscode_fn0, "big", bp_loc, line, col, res_array_len : len, roundtrip : roundtrip,
+                test_fn : async(result) =>
+                {
+
+                    var is_js = bp_loc.EndsWith(".js", StringComparison.Ordinal);
+                    var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = true,
+                            ownProperties = false
+                    }), ctx.token);
+                    if (is_js)
+                        await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+                    else
+                        AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+                    // Check for a __proto__ object
+                    // isOwn = true, accessorPropertiesOnly = false
+                    var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = false,
+                            ownProperties = true
+                    }), ctx.token);
+
+                    await CheckProps(obj_own.Value["result"], new
+                    {
+                        length = TNumber(len),
+                            // __proto__ = TArray (type, 0) // Is this one really required?
+                    }, $"obj_own", num_fields : is_js ? 2 : 1);
+
+                });
+        }
+
+        void CheckJFunction(JToken actual, string className, string label)
+        {
+            AssertEqual("function", actual["type"]?.Value<string>(), $"{label}-type");
+            AssertEqual(className, actual["className"]?.Value<string>(), $"{label}-className");
+        }
+
+        // This tests `callFunctionOn` with a function that the vscode-js-debug extension uses
+        // Using this here as a non-trivial test case
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, 10)]
+        [InlineData("big_array_js_test (0);", "/other.js", 8, 1, 0)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 10)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 0);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, 0)]
+        public async Task CheckVSCodeTestFunction2(string eval_fn, string bp_loc, int line, int col, int len)
+        {
+            var fetch_start_idx = 2;
+            var num_elems_fetch = 3;
+            string vscode_fn1 = "function(e,t){const r={},n=-1===e?0:e,i=-1===t?this.length:e+t;for(let e=n;e<i&&e<this.length;++e){const t=Object.getOwnPropertyDescriptor(this,e);t&&Object.defineProperty(r,e,t)}return r}";
+
+            await RunCallFunctionOn(eval_fn, vscode_fn1, "big", bp_loc, line, col,
+                fn_args : JArray.FromObject(new []
+                {
+                    new { @value = fetch_start_idx },
+                    new { @value = num_elems_fetch }
+                }),
+                test_fn : async(result) =>
+                {
+
+                    var is_js = bp_loc.EndsWith(".js", StringComparison.Ordinal);
+
+                    // isOwn = false, accessorPropertiesOnly = true
+                    var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = true,
+                            ownProperties = false
+                    }), ctx.token);
+                    if (is_js)
+                        await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+                    else
+                        AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+                    // Ignoring the __proto__ property
+
+                    // isOwn = true, accessorPropertiesOnly = false
+                    var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = false,
+                            ownProperties = true
+                    }), ctx.token);
+
+                    var obj_own_val = obj_own.Value["result"];
+                    var num_elems_recd = len == 0 ? 0 : num_elems_fetch;
+                    AssertEqual(is_js ? num_elems_recd + 1 : num_elems_recd, obj_own_val.Count(), $"obj_own-count");
+
+                    if (is_js)
+                        CheckObject(obj_own_val, "__proto__", "Object");
+
+                    for (int i = fetch_start_idx; i < fetch_start_idx + num_elems_recd; i++)
+                        CheckNumber(obj_own_val, i.ToString(), 1000 + i);
+                });
+        }
+
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+        public async Task RunOnArrayReturnEmptyArray(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+        {
+            var ret_len = 0;
+
+            await RunCallFunctionOn(eval_fn,
+                "function () { return []; }",
+                "big", bp_loc, line, col,
+                res_array_len : ret_len,
+                roundtrip : roundtrip,
+                test_fn : async(result) =>
+                {
+                    var is_js = bp_loc.EndsWith(".js", StringComparison.Ordinal);
+
+                    // getProperties (isOwn = false, accessorPropertiesOnly = true)
+                    var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = true,
+                            ownProperties = false
+                    }), ctx.token);
+                    if (is_js)
+                        await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+                    else
+                        AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+                    // getProperties (isOwn = true, accessorPropertiesOnly = false)
+                    var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = false,
+                            ownProperties = true
+                    }), ctx.token);
+
+                    await CheckProps(obj_own.Value["result"], new
+                    {
+                        length = TNumber(ret_len),
+                            // __proto__ returned by js
+                    }, $"obj_own", num_fields : is_js ? 2 : 1);
+                });
+        }
+
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+        public async Task RunOnArrayReturnArray(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+        {
+            var ret_len = 5;
+            await RunCallFunctionOn(eval_fn,
+                "function (m) { return Object.values (this).filter ((k, i) => i%m == 0); }",
+                "big", bp_loc, line, col,
+                fn_args : JArray.FromObject(new [] { new { value = 2 } }),
+                res_array_len : ret_len,
+                roundtrip : roundtrip,
+                test_fn : async(result) =>
+                {
+                    var is_js = bp_loc.EndsWith(".js");
+
+                    // getProperties (own=false)
+                    var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = true,
+                            ownProperties = false
+                    }), ctx.token);
+
+                    if (is_js)
+                        await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+                    else
+                        AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+                    // getProperties (own=true)
+                    // isOwn = true, accessorPropertiesOnly = false
+                    var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                    {
+                        objectId = result.Value["result"]["objectId"].Value<string>(),
+                            accessorPropertiesOnly = false,
+                            ownProperties = true
+                    }), ctx.token);
+
+                    // AssertEqual (2, obj_own.Value ["result"].Count (), $"{label}-obj_own.count");
+
+                    var obj_own_val = obj_own.Value["result"];
+                    await CheckProps(obj_own_val, new
+                    {
+                        length = TNumber(ret_len),
+                            // __proto__ returned by JS
+                    }, $"obj_own", num_fields: (is_js ? ret_len + 2 : ret_len + 1));
+
+                    for (int i = 0; i < ret_len; i++)
+                        CheckNumber(obj_own_val, i.ToString(), i * 2 + 1000);
+                });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task RunOnVTArray(bool roundtrip) => await RunCallFunctionOn(
+            "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);",
+            "function (m) { return Object.values (this).filter ((k, i) => i%m == 0); }",
+            "ss_arr",
+            "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12,
+            fn_args : JArray.FromObject(new [] { new { value = 2 } }),
+            res_array_len : 5,
+            roundtrip : roundtrip,
+            test_fn : async(result) =>
+            {
+                var ret_len = 5;
+
+                // getProperties (own=false)
+                var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                {
+                    objectId = result.Value["result"]["objectId"].Value<string>(),
+                        accessorPropertiesOnly = true,
+                        ownProperties = false
+                }), ctx.token);
+
+                AssertEqual(0, obj_accessors.Value["result"]?.Count(), "obj_accessors-count");
+
+                // getProperties (own=true)
+                // isOwn = true, accessorPropertiesOnly = false
+                var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                {
+                    objectId = result.Value["result"]["objectId"].Value<string>(),
+                        accessorPropertiesOnly = false,
+                        ownProperties = true
+                }), ctx.token);
+
+                var obj_own_val = obj_own.Value["result"];
+                await CheckProps(obj_own_val, new
+                {
+                    length = TNumber(ret_len),
+                        // __proto__ returned by JS
+                }, "obj_own", num_fields : ret_len + 1);
+
+                for (int i = 0; i < ret_len; i++)
+                {
+                    var act_i = CheckValueType(obj_own_val, i.ToString(), "Math.SimpleStruct");
+
+                    // Valuetypes can get sent as part of the container's getProperties, so ensure that we can access it
+                    var act_i_props = await GetProperties(act_i["value"]["objectId"]?.Value<string>());
+                    await CheckProps(act_i_props, new
+                    {
+                        dt = TValueType("System.DateTime", new DateTime(2020 + (i * 2), 1, 2, 3, 4, 5).ToString()),
+                            gs = TValueType("Math.GenericStruct<System.DateTime>")
+                    }, "obj_own ss_arr[{i}]");
+
+                    var gs_props = await GetObjectOnLocals(act_i_props, "gs");
+                    await CheckProps(gs_props, new
+                    {
+                        List = TObject("System.Collections.Generic.List<System.DateTime>", is_null : true),
+                            StringField = TString($"ss_arr # {i*2} # gs # StringField")
+                    }, "obj_own ss_arr[{i}].gs");
+
+                }
+            });
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task RunOnCFOValueTypeResult(bool roundtrip) => await RunCallFunctionOn(
+            eval_fn: "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);",
+            fn_decl: "function () { return this; }",
+            local_name: "simple_struct",
+            bp_loc: "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12,
+            roundtrip : roundtrip,
+            test_fn : async(result) =>
+            {
+
+                // getProperties (own=false)
+                var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                {
+                    objectId = result.Value["result"]["objectId"].Value<string>(),
+                        accessorPropertiesOnly = true,
+                        ownProperties = false
+                }), ctx.token);
+                AssertEqual(0, obj_accessors.Value["result"].Count(), "obj_accessors-count");
+
+                // getProperties (own=true)
+                // isOwn = true, accessorPropertiesOnly = false
+                var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                {
+                    objectId = result.Value["result"]["objectId"].Value<string>(),
+                        accessorPropertiesOnly = false,
+                        ownProperties = true
+                }), ctx.token);
+
+                var obj_own_val = obj_own.Value["result"];
+                var dt = new DateTime(2020, 1, 2, 3, 4, 5);
+                await CheckProps(obj_own_val, new
+                {
+                    dt = TValueType("System.DateTime", dt.ToString()),
+                        gs = TValueType("Math.GenericStruct<System.DateTime>")
+                }, $"obj_own-props");
+
+                await CheckDateTime(obj_own_val, "dt", dt);
+
+                var gs_props = await GetObjectOnLocals(obj_own_val, "gs");
+                await CheckProps(gs_props, new
+                {
+                    List = TObject("System.Collections.Generic.List<System.DateTime>", is_null : true),
+                        StringField = TString($"simple_struct # gs # StringField")
+                }, "simple_struct.gs-props");
+            });
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task RunOnJSObject(bool roundtrip) => await RunCallFunctionOn(
+            "object_js_test ();",
+            "function () { return this; }",
+            "obj", "/other.js", 17, 1,
+            fn_args : JArray.FromObject(new [] { new { value = 2 } }),
+            roundtrip : roundtrip,
+            test_fn : async(result) =>
+            {
+
+                // getProperties (own=false)
+                var obj_accessors = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                {
+                    objectId = result.Value["result"]["objectId"].Value<string>(),
+                        accessorPropertiesOnly = true,
+                        ownProperties = false
+                }), ctx.token);
+
+                await CheckProps(obj_accessors.Value["result"], new { __proto__ = TIgnore() }, "obj_accessors");
+
+                // getProperties (own=true)
+                // isOwn = true, accessorPropertiesOnly = false
+                var obj_own = await ctx.cli.SendCommand("Runtime.getProperties", JObject.FromObject(new
+                {
+                    objectId = result.Value["result"]["objectId"].Value<string>(),
+                        accessorPropertiesOnly = false,
+                        ownProperties = true
+                }), ctx.token);
+
+                var obj_own_val = obj_own.Value["result"];
+                await CheckProps(obj_own_val, new
+                {
+                    a_obj = TObject("Object"),
+                        b_arr = TArray("Array", 2)
+                }, "obj_own", num_fields : 3);
+            });
+
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+        public async Task RunOnArrayReturnObjectArrayByValue(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+        {
+            var ret_len = 5;
+            await RunCallFunctionOn(eval_fn,
+                "function () { return Object.values (this).filter ((k, i) => i%2 == 0); }",
+                "big", bp_loc, line, col, returnByValue : true, roundtrip : roundtrip,
+                test_fn : async(result) =>
+                {
+                    // Check cfo result
+                    AssertEqual(JTokenType.Object, result.Value["result"].Type, "cfo-result-jsontype");
+                    AssertEqual("object", result.Value["result"]["type"]?.Value<string>(), "cfo-res-type");
+
+                    AssertEqual(JTokenType.Array, result.Value["result"]["value"].Type, "cfo-res-value-jsontype");
+                    var actual = result.Value["result"] ? ["value"].Values<JToken>().ToArray();
+                    AssertEqual(ret_len, actual.Length, "cfo-res-value-length");
+
+                    for (int i = 0; i < ret_len; i++)
+                    {
+                        var exp_num = i * 2 + 1000;
+                        if (bp_loc.EndsWith(".js", StringComparison.Ordinal))
+                            AssertEqual(exp_num, actual[i].Value<int>(), $"[{i}]");
+                        else
+                        {
+                            AssertEqual("number", actual[i] ? ["type"]?.Value<string>(), $"[{i}]-type");
+                            AssertEqual(exp_num.ToString(), actual[i] ? ["description"]?.Value<string>(), $"[{i}]-description");
+                            AssertEqual(exp_num, actual[i] ? ["value"]?.Value<int>(), $"[{i}]-value");
+                        }
+                    }
+                    await Task.CompletedTask;
+                });
+        }
+
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+        public async Task RunOnArrayReturnArrayByValue(string eval_fn, string bp_loc, int line, int col, bool roundtrip) => await RunCallFunctionOn(eval_fn,
+            "function () { return Object.getOwnPropertyNames (this); }",
+            "big", bp_loc, line, col, returnByValue : true,
+            roundtrip : roundtrip,
+            test_fn : async(result) =>
+            {
+                // Check cfo result
+                AssertEqual("object", result.Value["result"]["type"]?.Value<string>(), "cfo-res-type");
+
+                var exp = new JArray();
+                for (int i = 0; i < 10; i++)
+                    exp.Add(i.ToString());
+                exp.Add("length");
+
+                var actual = result.Value["result"] ? ["value"];
+                if (!JObject.DeepEquals(exp, actual))
+                {
+                    Assert.True(false, $"Results don't match.\nExpected: {exp}\nActual:  {actual}");
+                }
+                await Task.CompletedTask;
+            });
+
+        [Theory]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, false)]
+        [InlineData("big_array_js_test (10);", "/other.js", 8, 1, true)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, true)]
+        public async Task RunOnArrayReturnPrimitive(string eval_fn, string bp_loc, int line, int col, bool return_by_val)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                await SetBreakpoint(bp_loc, line, col);
+
+                // callFunctionOn
+                var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);";
+                var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                // Um for js we get "scriptId": "6"
+                // CheckLocation (bp_loc, line, col, ctx.scripts, pause_location ["callFrames"][0]["location"]);
+
+                // Check the object at the bp
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+                var obj = GetAndAssertObjectWithName(frame_locals, "big");
+                var obj_id = obj["value"]["objectId"].Value<string>();
+
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = "function () { return 5; }",
+                        objectId = obj_id
+                });
+
+                // value of @returnByValue doesn't matter, as the returned value
+                // is a primitive
+                if (return_by_val)
+                    cfo_args["returnByValue"] = return_by_val;
+
+                // callFunctionOn
+                result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                await CheckValue(result.Value["result"], TNumber(5), "cfo-res");
+            });
+        }
+
+        public static TheoryData<string, string, int, int, bool?> SilentErrorsTestData(bool? silent) => new TheoryData<string, string, int, int, bool?>
+        { { "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:LocalsTest', 10);", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 23, 12, silent },
+            { "big_array_js_test (10);", "/other.js", 8, 1, silent }
+        };
+
+        [Theory]
+        [MemberData(nameof(SilentErrorsTestData), null)]
+        [MemberData(nameof(SilentErrorsTestData), false)]
+        [MemberData(nameof(SilentErrorsTestData), true)]
+        public async Task CFOWithSilentReturnsErrors(string eval_fn, string bp_loc, int line, int col, bool? silent)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                await SetBreakpoint(bp_loc, line, col);
+
+                // callFunctionOn
+                var eval_expr = "window.setTimeout(function() { " + eval_fn + " }, 1);";
+                var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+                var obj = GetAndAssertObjectWithName(frame_locals, "big");
+                var big_obj_id = obj["value"]["objectId"].Value<string>();
+                var error_msg = "#This is an error message#";
+
+                // Check the object at the bp
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = $"function () {{ throw Error ('{error_msg}'); }}",
+                        objectId = big_obj_id
+                });
+
+                if (silent.HasValue)
+                    cfo_args["silent"] = silent;
+
+                // callFunctionOn, Silent does not change the result, except that the error
+                // doesn't get reported, and the execution is NOT paused even with setPauseOnException=true
+                result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                Assert.False(result.IsOk, "result.IsOk");
+                Assert.True(result.IsErr, "result.IsErr");
+
+                var hasErrorMessage = result.Error["exceptionDetails"] ? ["exception"] ? ["description"]?.Value<string>()?.Contains(error_msg);
+                Assert.True((hasErrorMessage ?? false), "Exception message not found");
+            });
+        }
+
+        public static TheoryData<string, string, int, int, string, Func<string[], object>, bool> GettersTestData(bool use_cfo) => new TheoryData<string, string, int, int, string, Func<string[], object>, bool>
+        {
+            // Chrome sends this one
+            {
+                "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');",
+                "PropertyGettersTest",
+                30,
+                12,
+                "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i<n;++i){ result=result[properties[i]]; } return result; }",
+                (arg_strs) => JArray.FromObject(arg_strs).ToString(),
+                use_cfo
+            },
+            {
+                "invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');",
+                "MoveNext",
+                38,
+                12,
+                "function invokeGetter(arrayStr){ let result=this; const properties=JSON.parse(arrayStr); for(let i=0,n=properties.length;i<n;++i){ result=result[properties[i]]; } return result; }",
+                (arg_strs) => JArray.FromObject(arg_strs).ToString(),
+                use_cfo
+            },
+
+            // VSCode sends this one
+            {
+                "invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');",
+                "PropertyGettersTest",
+                30,
+                12,
+                "function(e){return this[e]}",
+                (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty,
+                use_cfo
+            },
+            {
+                "invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');",
+                "MoveNext",
+                38,
+                12,
+                "function(e){return this[e]}",
+                (args_str) => args_str?.Length > 0 ? args_str[0] : String.Empty,
+                use_cfo
+            }
+        };
+
+        [Theory]
+        [MemberData(nameof(GettersTestData), parameters : false)]
+        [MemberData(nameof(GettersTestData), parameters : true)]
+        public async Task PropertyGettersOnObjectsTest(string eval_fn, string method_name, int line, int col, string cfo_fn, Func<string[], object> get_args_fn, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col,
+            method_name,
+            $"window.setTimeout(function() {{ {eval_fn} }}, 1);",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var dt = new DateTime(10, 9, 8, 7, 6, 5);
+
+                await CheckProps(frame_locals, new
+                {
+                    ptd = TObject("DebuggerTests.ClassWithProperties"),
+                        swp = TObject("DebuggerTests.StructWithProperties"),
+                }, "locals#0");
+
+                var ptd = GetAndAssertObjectWithName(frame_locals, "ptd");
+
+                var ptd_props = await GetProperties(ptd?["value"] ? ["objectId"]?.Value<string>());
+                await CheckProps(ptd_props, new
+                {
+                    Int = TGetter("Int"),
+                        String = TGetter("String"),
+                        DT = TGetter("DT"),
+                        IntArray = TGetter("IntArray"),
+                        DTArray = TGetter("DTArray")
+                }, "ptd", num_fields : 7);
+
+                // Automatic properties don't have invokable getters, because we can get their
+                // value from the backing field directly
+                {
+                    dt = new DateTime(4, 5, 6, 7, 8, 9);
+                    var dt_auto_props = await GetObjectOnLocals(ptd_props, "DTAutoProperty");
+                    await CheckDateTime(ptd_props, "DTAutoProperty", dt);
+                }
+
+                // Invoke getters, and check values
+
+                var res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "Int" }));
+                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+                await CheckValue(res.Value["result"], JObject.FromObject(new { type = "number", value = 5 }), "ptd.Int");
+
+                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "String" }));
+                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+                await CheckValue(res.Value["result"], JObject.FromObject(new { type = "string", value = "foobar" }), "ptd.String");
+
+                dt = new DateTime(3, 4, 5, 6, 7, 8);
+                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DT" }));
+                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+                await CheckValue(res.Value["result"], TValueType("System.DateTime", dt.ToString()), "ptd.DT");
+                await CheckDateTimeValue(res.Value["result"], dt);
+
+                // Check arrays through getters
+
+                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "IntArray" }));
+                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+                await CheckValue(res.Value["result"], TArray("int[]", 2), "ptd.IntArray");
+                {
+                    var arr_elems = await GetProperties(res.Value["result"] ? ["objectId"]?.Value<string>());
+                    var exp_elems = new []
+                    {
+                        TNumber(10),
+                        TNumber(20)
+                    };
+
+                    await CheckProps(arr_elems, exp_elems, "ptd.IntArray");
+                }
+
+                res = await InvokeGetter(ptd, cfo_fn, get_args_fn(new [] { "DTArray" }));
+                Assert.True(res.IsOk, $"InvokeGetter failed with : {res}");
+                await CheckValue(res.Value["result"], TArray("System.DateTime[]", 2), "ptd.DTArray");
+                {
+                    var dt0 = new DateTime(6, 7, 8, 9, 10, 11);
+                    var dt1 = new DateTime(1, 2, 3, 4, 5, 6);
+
+                    var arr_elems = await GetProperties(res.Value["result"] ? ["objectId"]?.Value<string>());
+                    var exp_elems = new []
+                    {
+                        TValueType("System.DateTime", dt0.ToString()),
+                        TValueType("System.DateTime", dt1.ToString()),
+                    };
+
+                    await CheckProps(arr_elems, exp_elems, "ptd.DTArray");
+                }
+            });
+
+        [Theory]
+        [InlineData("invoke_static_method_async ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTestAsync');", "MoveNext", 38, 12)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "PropertyGettersTest", 30, 12)]
+        public async Task PropertyGettersOnStructsTest(string eval_fn, string method_name, int line, int col) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-cfo-test.cs", line, col,
+            method_name,
+            $"window.setTimeout(function() {{ {eval_fn} }}, 1);",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    ptd = TObject("DebuggerTests.ClassWithProperties"),
+                        swp = TObject("DebuggerTests.StructWithProperties"),
+                }, "locals#0");
+
+                var swp = GetAndAssertObjectWithName(frame_locals, "swp");
+
+                var swp_props = await GetProperties(swp?["value"] ? ["objectId"]?.Value<string>());
+                await CheckProps(swp_props, new
+                {
+                    Int = TSymbol("int { get; }"),
+                        String = TSymbol("string { get; }"),
+                        DT = TSymbol("System.DateTime { get; }"),
+                        IntArray = TSymbol("int[] { get; }"),
+                        DTArray = TSymbol("System.DateTime[] { get; }")
+                }, "swp");
+            });
+
+        [Theory]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, false)]
+        [InlineData("invoke_static_method ('[debugger-test] DebuggerTests.CallFunctionOnTest:PropertyGettersTest');", "dotnet://debugger-test.dll/debugger-cfo-test.cs", 30, 12, true)]
+        [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, false)]
+        [InlineData("invoke_getters_js_test ();", "/other.js", 29, 1, true)]
+        public async Task CheckAccessorsOnObjectsWithCFO(string eval_fn, string bp_loc, int line, int col, bool roundtrip)
+        {
+            await RunCallFunctionOn(
+                eval_fn, "function() { return this; }", "ptd",
+                bp_loc, line, col,
+                roundtrip : roundtrip,
+                test_fn : async(result) =>
+                {
+
+                    var is_js = bp_loc.EndsWith(".js");
+
+                    // Check with `accessorPropertiesOnly=true`
+
+                    var id = result.Value?["result"] ? ["objectId"]?.Value<string>();
+                    var get_prop_req = JObject.FromObject(new
+                    {
+                        objectId = id,
+                            accessorPropertiesOnly = true
+                    });
+
+                    var res = await GetPropertiesAndCheckAccessors(get_prop_req, is_js ? 6 : 5); // js returns extra `__proto__` member also
+                    Assert.False(res.Value["result"].Any(jt => jt["name"]?.Value<string>() == "StringField"), "StringField shouldn't be returned for `accessorPropertiesOnly`");
+
+                    // Check with `accessorPropertiesOnly` unset, == false
+                    get_prop_req = JObject.FromObject(new
+                    {
+                        objectId = id,
+                    });
+
+                    res = await GetPropertiesAndCheckAccessors(get_prop_req, is_js ? 8 : 7); // js returns a `__proto__` member also
+                    Assert.True(res.Value["result"].Any(jt => jt["name"]?.Value<string>() == "StringField"), "StringField should be returned for `accessorPropertiesOnly=false`");
+                });
+
+            async Task<Result> GetPropertiesAndCheckAccessors(JObject get_prop_req, int num_fields)
+            {
+                var res = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token);
+                if (!res.IsOk)
+                    Assert.True(false, $"Runtime.getProperties failed for {get_prop_req.ToString ()}, with Result: {res}");
+
+                var accessors = new string[] { "Int", "String", "DT", "IntArray", "DTArray" };
+                foreach (var name in accessors)
+                {
+                    var prop = GetAndAssertObjectWithName(res.Value["result"], name);
+                    Assert.True(prop["value"] == null, $"{name} shouldn't have a `value`");
+
+                    await CheckValue(prop, TGetter(name), $"{name}");
+                }
+
+                return res;
+            }
+        }
+
+        async Task<Result> InvokeGetter(JToken obj, string fn, object arguments) => await ctx.cli.SendCommand(
+            "Runtime.callFunctionOn",
+            JObject.FromObject(new
+            {
+                functionDeclaration = fn,
+                    objectId = obj["value"] ? ["objectId"]?.Value<string>(),
+                    arguments = new [] { new { value = arguments } }
+            }), ctx.token);
+
+        /*
+         * 1. runs `Runtime.callFunctionOn` on the objectId,
+         * if @roundtrip == false, then
+         *     -> calls @test_fn for that result (new objectId)
+         * else
+         *     -> runs it again on the *result's* objectId.
+         *        -> calls @test_fn on the *new* result objectId
+         *
+         * Returns: result of `Runtime.callFunctionOn`
+         */
+        async Task RunCallFunctionOn(string eval_fn, string fn_decl, string local_name, string bp_loc, int line, int col, int res_array_len = -1,
+            Func<Result, Task> test_fn = null, bool returnByValue = false, JArray fn_args = null, bool roundtrip = false)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                await SetBreakpoint(bp_loc, line, col);
+
+                // callFunctionOn
+                var eval_expr = $"window.setTimeout(function() {{ {eval_fn} }}, 1);";
+                var result = await ctx.cli.SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = eval_expr }), ctx.token);
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                // Um for js we get "scriptId": "6"
+                // CheckLocation (bp_loc, line, col, ctx.scripts, pause_location ["callFrames"][0]["location"]);
+
+                // Check the object at the bp
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["scopeChain"][0]["object"]["objectId"].Value<string>());
+                var obj = GetAndAssertObjectWithName(frame_locals, local_name);
+                var obj_id = obj["value"]["objectId"].Value<string>();
+
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = fn_decl,
+                        objectId = obj_id
+                });
+
+                if (fn_args != null)
+                    cfo_args["arguments"] = fn_args;
+
+                if (returnByValue)
+                    cfo_args["returnByValue"] = returnByValue;
+
+                // callFunctionOn
+                result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                await CheckCFOResult(result);
+
+                // If it wasn't `returnByValue`, then try to run a new function
+                // on that *returned* object
+                // This second function, just returns the object as-is, so the same
+                // test_fn is re-usable.
+                if (!returnByValue && roundtrip)
+                {
+                    cfo_args = JObject.FromObject(new
+                    {
+                        functionDeclaration = "function () { return this; }",
+                            objectId = result.Value["result"]["objectId"]?.Value<string>()
+                    });
+
+                    if (fn_args != null)
+                        cfo_args["arguments"] = fn_args;
+
+                    result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+
+                    await CheckCFOResult(result);
+                }
+
+                if (test_fn != null)
+                    await test_fn(result);
+
+                return;
+
+                async Task CheckCFOResult(Result result)
+                {
+                    if (returnByValue)
+                        return;
+
+                    if (res_array_len < 0)
+                        await CheckValue(result.Value["result"], TObject("Object"), $"cfo-res");
+                    else
+                        await CheckValue(result.Value["result"], TArray("Array", res_array_len), $"cfo-res");
+                }
+            });
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs
new file mode 100644 (file)
index 0000000..6b7a560
--- /dev/null
@@ -0,0 +1,67 @@
+// 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.Globalization;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace DebuggerTests
+{
+    public class DateTimeList : DebuggerTestBase
+    {
+
+        [Theory]
+        [InlineData("en-US")]
+
+        // Currently not passing tests. Issue #19743
+        // [InlineData ("ja-JP")]
+        // [InlineData ("es-ES")]
+        //[InlineData ("de-DE")]
+        //[InlineData ("ka-GE")]
+        //[InlineData ("hu-HU")]
+        public async Task CheckDateTimeLocale(string locale)
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-datetime-test.cs";
+
+                await SetBreakpointInMethod("debugger-test", "DebuggerTests.DateTimeTest", "LocaleTest", 15);
+
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.DateTimeTest:LocaleTest'," +
+                    $"'{locale}'); }}, 1);",
+                    debugger_test_loc, 25, 12, "LocaleTest",
+                    locals_fn : async(locals) =>
+                    {
+                        DateTimeFormatInfo dtfi = CultureInfo.GetCultureInfo(locale).DateTimeFormat;
+                        CultureInfo.CurrentCulture = new CultureInfo(locale, false);
+                        DateTime dt = new DateTime(2020, 1, 2, 3, 4, 5);
+                        string dt_str = dt.ToString();
+
+                        var fdtp = dtfi.FullDateTimePattern;
+                        var ldp = dtfi.LongDatePattern;
+                        var ltp = dtfi.LongTimePattern;
+                        var sdp = dtfi.ShortDatePattern;
+                        var stp = dtfi.ShortTimePattern;
+
+                        CheckString(locals, "fdtp", fdtp);
+                        CheckString(locals, "ldp", ldp);
+                        CheckString(locals, "ltp", ltp);
+                        CheckString(locals, "sdp", sdp);
+                        CheckString(locals, "stp", stp);
+                        await CheckDateTime(locals, "dt", dt);
+                        CheckString(locals, "dt_str", dt_str);
+                    }
+                );
+
+            });
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj
new file mode 100644 (file)
index 0000000..09b79eb
--- /dev/null
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="xunit" Version="2.4.0" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
+
+    <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\BrowserDebugHost\BrowserDebugHost.csproj" />
+    <ProjectReference Include="..\BrowserDebugProxy\BrowserDebugProxy.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs
new file mode 100644 (file)
index 0000000..e186ddc
--- /dev/null
@@ -0,0 +1,306 @@
+// 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 Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+    public class DelegateTests : DebuggerTestBase
+    {
+
+        [Theory]
+        [InlineData(0, 53, 8, "DelegatesTest", false)]
+        [InlineData(0, 53, 8, "DelegatesTest", true)]
+        [InlineData(2, 99, 8, "InnerMethod2", false)]
+        [InlineData(2, 99, 8, "InnerMethod2", true)]
+        public async Task InspectLocalsWithDelegatesAtBreakpointSite(int frame, int line, int col, string method_name, bool use_cfo) =>
+            await CheckInspectLocalsAtBreakpointSite(
+                "dotnet://debugger-test.dll/debugger-test.cs", line, col, method_name,
+                "window.setTimeout(function() { invoke_delegates_test (); }, 1);",
+                use_cfo : use_cfo,
+                wait_for_event_fn : async(pause_location) =>
+                {
+                    var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+                    await CheckProps(locals, new
+                    {
+                        fn_func = TDelegate("System.Func<Math, bool>", "bool <DelegatesTest>|(Math)"),
+                            fn_func_null = TObject("System.Func<Math, bool>", is_null : true),
+                            fn_func_arr = TArray("System.Func<Math, bool>[]", 1),
+                            fn_del = TDelegate("Math.IsMathNull", "bool IsMathNullDelegateTarget (Math)"),
+                            fn_del_null = TObject("Math.IsMathNull", is_null : true),
+                            fn_del_arr = TArray("Math.IsMathNull[]", 1),
+
+                            // Unused locals
+                            fn_func_unused = TDelegate("System.Func<Math, bool>", "bool <DelegatesTest>|(Math)"),
+                            fn_func_null_unused = TObject("System.Func<Math, bool>", is_null : true),
+                            fn_func_arr_unused = TArray("System.Func<Math, bool>[]", 1),
+
+                            fn_del_unused = TDelegate("Math.IsMathNull", "bool IsMathNullDelegateTarget (Math)"),
+                            fn_del_null_unused = TObject("Math.IsMathNull", is_null : true),
+                            fn_del_arr_unused = TArray("Math.IsMathNull[]", 1),
+
+                            res = TBool(false),
+                            m_obj = TObject("Math")
+                    }, "locals");
+
+                    await CompareObjectPropertiesFor(locals, "fn_func_arr", new []
+                    {
+                        TDelegate(
+                            "System.Func<Math, bool>",
+                            "bool <DelegatesTest>|(Math)")
+                    }, "locals#fn_func_arr");
+
+                    await CompareObjectPropertiesFor(locals, "fn_del_arr", new []
+                    {
+                        TDelegate(
+                            "Math.IsMathNull",
+                            "bool IsMathNullDelegateTarget (Math)")
+                    }, "locals#fn_del_arr");
+
+                    await CompareObjectPropertiesFor(locals, "fn_func_arr_unused", new []
+                    {
+                        TDelegate(
+                            "System.Func<Math, bool>",
+                            "bool <DelegatesTest>|(Math)")
+                    }, "locals#fn_func_arr_unused");
+
+                    await CompareObjectPropertiesFor(locals, "fn_del_arr_unused", new []
+                    {
+                        TDelegate(
+                            "Math.IsMathNull",
+                            "bool IsMathNullDelegateTarget (Math)")
+                    }, "locals#fn_del_arr_unused");
+                }
+            );
+
+        [Theory]
+        [InlineData(0, 202, 8, "DelegatesSignatureTest", false)]
+        [InlineData(0, 202, 8, "DelegatesSignatureTest", true)]
+        [InlineData(2, 99, 8, "InnerMethod2", false)]
+        [InlineData(2, 99, 8, "InnerMethod2", true)]
+        public async Task InspectDelegateSignaturesWithFunc(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-test.cs",
+            line, col,
+            bp_method,
+            "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:DelegatesSignatureTest'); }, 1)",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+                await CheckProps(locals, new
+                {
+                    fn_func = TDelegate("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>",
+                            "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+                        fn_func_del = TDelegate("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>",
+                            "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+                        fn_func_null = TObject("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>", is_null : true),
+                        fn_func_only_ret = TDelegate("System.Func<bool>", "bool <DelegatesSignatureTest>|()"),
+                        fn_func_arr = TArray("System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>[]", 1),
+
+                        fn_del = TDelegate("Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+                        fn_del_l = TDelegate("Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+
+                        fn_del_null = TObject("Math.DelegateForSignatureTest", is_null : true),
+                        fn_del_arr = TArray("Math.DelegateForSignatureTest[]", 2),
+                        m_obj = TObject("Math"),
+                        gs_gs = TValueType("Math.GenericStruct<Math.GenericStruct<int[]>>"),
+                        fn_void_del = TDelegate("Math.DelegateWithVoidReturn",
+                            "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)"),
+
+                        fn_void_del_arr = TArray("Math.DelegateWithVoidReturn[]", 1),
+                        fn_void_del_null = TObject("Math.DelegateWithVoidReturn", is_null : true),
+                        gs = TValueType("Math.GenericStruct<int[]>"),
+                        rets = TArray("Math.GenericStruct<bool[]>[]", 6)
+                }, "locals");
+
+                await CompareObjectPropertiesFor(locals, "fn_func_arr", new []
+                {
+                    TDelegate(
+                        "System.Func<Math, Math.GenericStruct<Math.GenericStruct<int[]>>, Math.GenericStruct<bool[]>>",
+                        "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+                }, "locals#fn_func_arr");
+
+                await CompareObjectPropertiesFor(locals, "fn_del_arr", new []
+                {
+                    TDelegate(
+                            "Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+                        TDelegate(
+                            "Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> <DelegatesSignatureTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)")
+                }, "locals#fn_del_arr");
+
+                await CompareObjectPropertiesFor(locals, "fn_void_del_arr", new []
+                {
+                    TDelegate(
+                        "Math.DelegateWithVoidReturn",
+                        "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)")
+                }, "locals#fn_void_del_arr");
+            });
+
+        [Theory]
+        [InlineData(0, 224, 8, "ActionTSignatureTest", false)]
+        [InlineData(0, 224, 8, "ActionTSignatureTest", true)]
+        [InlineData(2, 99, 8, "InnerMethod2", false)]
+        [InlineData(2, 99, 8, "InnerMethod2", true)]
+        public async Task ActionTSignatureTest(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-test.cs", line, col,
+            bp_method,
+            "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:ActionTSignatureTest'); }, 1)",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+                await CheckProps(locals, new
+                {
+                    fn_action = TDelegate("System.Action<Math.GenericStruct<int[]>>",
+                            "void <ActionTSignatureTest>|(Math.GenericStruct<int[]>)"),
+                        fn_action_del = TDelegate("System.Action<Math.GenericStruct<int[]>>",
+                            "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)"),
+                        fn_action_bare = TDelegate("System.Action",
+                            "void|()"),
+
+                        fn_action_null = TObject("System.Action<Math.GenericStruct<int[]>>", is_null : true),
+
+                        fn_action_arr = TArray("System.Action<Math.GenericStruct<int[]>>[]", 3),
+
+                        gs = TValueType("Math.GenericStruct<int[]>"),
+                }, "locals");
+
+                await CompareObjectPropertiesFor(locals, "fn_action_arr", new []
+                {
+                    TDelegate(
+                            "System.Action<Math.GenericStruct<int[]>>",
+                            "void <ActionTSignatureTest>|(Math.GenericStruct<int[]>)"),
+                        TDelegate(
+                            "System.Action<Math.GenericStruct<int[]>>",
+                            "void DelegateTargetWithVoidReturn (Math.GenericStruct<int[]>)"),
+                        TObject("System.Action<Math.GenericStruct<int[]>>", is_null : true)
+                }, "locals#fn_action_arr");
+            });
+
+        [Theory]
+        [InlineData(0, 242, 8, "NestedDelegatesTest", false)]
+        [InlineData(0, 242, 8, "NestedDelegatesTest", true)]
+        [InlineData(2, 99, 8, "InnerMethod2", false)]
+        [InlineData(2, 99, 8, "InnerMethod2", true)]
+        public async Task NestedDelegatesTest(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-test.cs", line, col,
+            bp_method,
+            "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:NestedDelegatesTest'); }, 1)",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+                await CheckProps(locals, new
+                {
+                    fn_func = TDelegate("System.Func<System.Func<int, bool>, bool>",
+                            "bool <NestedDelegatesTest>|(Func<int, bool>)"),
+                        fn_func_null = TObject("System.Func<System.Func<int, bool>, bool>", is_null : true),
+                        fn_func_arr = TArray("System.Func<System.Func<int, bool>, bool>[]", 1),
+                        fn_del_arr = TArray("System.Func<System.Func<int, bool>, bool>[]", 1),
+
+                        m_obj = TObject("Math"),
+                        fn_del_null = TObject("System.Func<System.Func<int, bool>, bool>", is_null : true),
+                        fs = TDelegate("System.Func<int, bool>",
+                            "bool <NestedDelegatesTest>|(int)")
+                }, "locals");
+
+                await CompareObjectPropertiesFor(locals, "fn_func_arr", new []
+                {
+                    TDelegate(
+                        "System.Func<System.Func<int, bool>, bool>",
+                        "bool <NestedDelegatesTest>|(System.Func<int, bool>)")
+                }, "locals#fn_func_arr");
+
+                await CompareObjectPropertiesFor(locals, "fn_del_arr", new []
+                {
+                    TDelegate(
+                        "System.Func<System.Func<int, bool>, bool>",
+                        "bool DelegateTargetForNestedFunc (Func<int, bool>)")
+                }, "locals#fn_del_arr");
+            });
+
+        [Theory]
+        [InlineData(0, 262, 8, "MethodWithDelegateArgs", false)]
+        [InlineData(0, 262, 8, "MethodWithDelegateArgs", true)]
+        [InlineData(2, 99, 8, "InnerMethod2", false)]
+        [InlineData(2, 99, 8, "InnerMethod2", true)]
+        public async Task DelegatesAsMethodArgsTest(int frame, int line, int col, string bp_method, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-test.cs", line, col,
+            bp_method,
+            "window.setTimeout (function () { invoke_static_method ('[debugger-test] Math:DelegatesAsMethodArgsTest'); }, 1)",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][frame]["callFrameId"].Value<string>());
+
+                await CheckProps(locals, new
+                {
+                    @this = TObject("Math"),
+                        dst_arr = TArray("Math.DelegateForSignatureTest[]", 2),
+                        fn_func = TDelegate("System.Func<char[], bool>",
+                            "bool <DelegatesAsMethodArgsTest>|(char[])"),
+                        fn_action = TDelegate("System.Action<Math.GenericStruct<int>[]>",
+                            "void <DelegatesAsMethodArgsTest>|(Math.GenericStruct<int>[])")
+                }, "locals");
+
+                await CompareObjectPropertiesFor(locals, "dst_arr", new []
+                {
+                    TDelegate("Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+                        TDelegate("Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> <DelegatesAsMethodArgsTest>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+                }, "locals#dst_arr");
+            });
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task MethodWithDelegatesAsyncTest(bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-test.cs", 281, 8,
+            "MoveNext", //"DelegatesAsMethodArgsTestAsync"
+            "window.setTimeout (function () { invoke_static_method_async ('[debugger-test] Math:MethodWithDelegatesAsyncTest'); }, 1)",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                await CheckProps(locals, new
+                {
+                    @this = TObject("Math"),
+                        _dst_arr = TArray("Math.DelegateForSignatureTest[]", 2),
+                        _fn_func = TDelegate("System.Func<char[], bool>",
+                            "bool <MethodWithDelegatesAsync>|(char[])"),
+                        _fn_action = TDelegate("System.Action<Math.GenericStruct<int>[]>",
+                            "void <MethodWithDelegatesAsync>|(Math.GenericStruct<int>[])")
+                }, "locals");
+
+                await CompareObjectPropertiesFor(locals, "_dst_arr", new []
+                {
+                    TDelegate(
+                            "Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> DelegateTargetForSignatureTest (Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+                        TDelegate(
+                            "Math.DelegateForSignatureTest",
+                            "Math.GenericStruct<bool[]> <MethodWithDelegatesAsync>|(Math,Math.GenericStruct<Math.GenericStruct<int[]>>)"),
+                }, "locals#dst_arr");
+            });
+    }
+
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs
new file mode 100644 (file)
index 0000000..bedba13
--- /dev/null
@@ -0,0 +1,167 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    internal class DevToolsClient : IDisposable
+    {
+        ClientWebSocket socket;
+        List<Task> pending_ops = new List<Task>();
+        TaskCompletionSource<bool> side_exit = new TaskCompletionSource<bool>();
+        List<byte[]> pending_writes = new List<byte[]>();
+        Task current_write;
+        readonly ILogger logger;
+
+        public DevToolsClient(ILogger logger)
+            {
+                this.logger = logger;
+            }
+
+            ~DevToolsClient()
+            {
+                Dispose(false);
+            }
+
+        public void Dispose()
+        {
+            Dispose(true);
+        }
+
+        public async Task Close(CancellationToken cancellationToken)
+        {
+            if (socket.State == WebSocketState.Open)
+                await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (disposing)
+                socket.Dispose();
+        }
+
+        Task Pump(Task task, CancellationToken token)
+        {
+            if (task != current_write)
+                return null;
+            current_write = null;
+
+            pending_writes.RemoveAt(0);
+
+            if (pending_writes.Count > 0)
+            {
+                current_write = socket.SendAsync(new ArraySegment<byte>(pending_writes[0]), WebSocketMessageType.Text, true, token);
+                return current_write;
+            }
+            return null;
+        }
+
+        async Task<string> ReadOne(CancellationToken token)
+        {
+            byte[] buff = new byte[4000];
+            var mem = new MemoryStream();
+            while (true)
+            {
+                var result = await this.socket.ReceiveAsync(new ArraySegment<byte>(buff), token);
+                if (result.MessageType == WebSocketMessageType.Close)
+                {
+                    return null;
+                }
+
+                if (result.EndOfMessage)
+                {
+                    mem.Write(buff, 0, result.Count);
+                    return Encoding.UTF8.GetString(mem.GetBuffer(), 0, (int) mem.Length);
+                }
+                else
+                {
+                    mem.Write(buff, 0, result.Count);
+                }
+            }
+        }
+
+        protected void Send(byte[] bytes, CancellationToken token)
+        {
+            pending_writes.Add(bytes);
+            if (pending_writes.Count == 1)
+            {
+                if (current_write != null)
+                    throw new Exception("Internal state is bad. current_write must be null if there are no pending writes");
+
+                current_write = socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, token);
+                pending_ops.Add(current_write);
+            }
+        }
+
+        async Task MarkCompleteAfterward(Func<CancellationToken, Task> send, CancellationToken token)
+        {
+            try
+            {
+                await send(token);
+                side_exit.SetResult(true);
+            }
+            catch (Exception e)
+            {
+                side_exit.SetException(e);
+            }
+        }
+
+        protected async Task<bool> ConnectWithMainLoops(
+            Uri uri,
+            Func<string, CancellationToken, Task> receive,
+            Func<CancellationToken, Task> send,
+            CancellationToken token)
+        {
+
+            logger.LogDebug("connecting to {0}", uri);
+            this.socket = new ClientWebSocket();
+            this.socket.Options.KeepAliveInterval = Timeout.InfiniteTimeSpan;
+
+            await this.socket.ConnectAsync(uri, token);
+            pending_ops.Add(ReadOne(token));
+            pending_ops.Add(side_exit.Task);
+            pending_ops.Add(MarkCompleteAfterward(send, token));
+
+            while (!token.IsCancellationRequested)
+            {
+                var task = await Task.WhenAny(pending_ops);
+                if (task == pending_ops[0])
+                { //pending_ops[0] is for message reading
+                    var msg = ((Task<string>) task).Result;
+                    pending_ops[0] = ReadOne(token);
+                    Task tsk = receive(msg, token);
+                    if (tsk != null)
+                        pending_ops.Add(tsk);
+                }
+                else if (task == pending_ops[1])
+                {
+                    var res = ((Task<bool>) task).Result;
+                    //it might not throw if exiting successfull
+                    return res;
+                }
+                else
+                { //must be a background task
+                    pending_ops.Remove(task);
+                    var tsk = Pump(task, token);
+                    if (tsk != null)
+                        pending_ops.Add(tsk);
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual void Log(string priority, string msg)
+        {
+            //
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs
new file mode 100644 (file)
index 0000000..f2836d3
--- /dev/null
@@ -0,0 +1,214 @@
+// 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 Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+    public class EvaluateOnCallFrameTests : DebuggerTestBase
+    {
+
+        [Fact]
+        public async Task EvaluateThisProperties() => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+            "run",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a");
+                CheckContentValue(evaluate, "1");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "b");
+                CheckContentValue(evaluate, "2");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "c");
+                CheckContentValue(evaluate, "3");
+
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "dt");
+                await CheckDateTimeValue(evaluate, new DateTime(2000, 5, 4, 3, 2, 1));
+            });
+
+        [Theory]
+        [InlineData(63, 12, "EvaluateTestsStructInstanceMethod")]
+        [InlineData(79, 12, "GenericInstanceMethodOnStruct<int>")]
+        [InlineData(102, 12, "EvaluateTestsGenericStructInstanceMethod")]
+        public async Task EvaluateThisPropertiesOnStruct(int line, int col, string method_name) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", line, col,
+            method_name,
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a");
+                CheckContentValue(evaluate, "1");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "b");
+                CheckContentValue(evaluate, "2");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "c");
+                CheckContentValue(evaluate, "3");
+
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "dateTime");
+                await CheckDateTimeValue(evaluate, new DateTime(2020, 1, 2, 3, 4, 5));
+            });
+
+        [Fact]
+        public async Task EvaluateParameters() => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+            "run",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "g");
+                CheckContentValue(evaluate, "100");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "h");
+                CheckContentValue(evaluate, "200");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "valString");
+                CheckContentValue(evaluate, "test");
+            });
+
+        [Fact]
+        public async Task EvaluateLocals() => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+            "run",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "d");
+                CheckContentValue(evaluate, "101");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "e");
+                CheckContentValue(evaluate, "102");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "f");
+                CheckContentValue(evaluate, "103");
+
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_dt");
+                await CheckDateTimeValue(evaluate, new DateTime(2010, 9, 8, 7, 6, 5));
+            });
+
+        [Fact]
+        public async Task EvaluateLocalsAsync()
+        {
+            var bp_loc = "dotnet://debugger-test.dll/debugger-array-test.cs";
+            int line = 249;
+            int col = 12;
+            var function_name = "MoveNext";
+            await CheckInspectLocalsAtBreakpointSite(
+                bp_loc, line, col,
+                function_name,
+                "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.ArrayTestsClass:EntryPointForStructMethod', true); })",
+                wait_for_event_fn : async(pause_location) =>
+                {
+                    var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                    // sc_arg
+                    {
+                        var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "sc_arg");
+                        await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#1");
+
+                        var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
+                        await CheckProps(sc_arg_props, new
+                        {
+                            X = TNumber(10),
+                                Y = TNumber(45),
+                                Id = TString("sc#Id"),
+                                Color = TEnum("DebuggerTests.RGB", "Blue"),
+                                PointWithCustomGetter = TGetter("PointWithCustomGetter")
+                        }, "sc_arg_props#1");
+                    }
+
+                    // local_gs
+                    {
+                        var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_gs");
+                        await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), "local_gs#1");
+
+                        var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
+                        await CheckProps(local_gs_props, new
+                        {
+                            Id = TObject("string", is_null : true),
+                                Color = TEnum("DebuggerTests.RGB", "Red"),
+                                Value = TNumber(0)
+                        }, "local_gs_props#1");
+                    }
+
+                    // step, check local_gs
+                    pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 1, col, function_name);
+                    {
+                        var local_gs = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "local_gs");
+                        await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct<int>"), "local_gs#2");
+
+                        var local_gs_props = await GetProperties(local_gs["objectId"]?.Value<string>());
+                        await CheckProps(local_gs_props, new
+                        {
+                            Id = TString("local_gs#Id"),
+                                Color = TEnum("DebuggerTests.RGB", "Green"),
+                                Value = TNumber(4)
+                        }, "local_gs_props#2");
+                    }
+
+                    // step check sc_arg.Id
+                    pause_location = await StepAndCheck(StepKind.Over, bp_loc, line + 2, col, function_name);
+                    {
+                        var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "sc_arg");
+                        await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#2");
+
+                        var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value<string>());
+                        await CheckProps(sc_arg_props, new
+                        {
+                            X = TNumber(10),
+                                Y = TNumber(45),
+                                Id = TString("sc_arg#Id"), // <------- This changed
+                                Color = TEnum("DebuggerTests.RGB", "Blue"),
+                                PointWithCustomGetter = TGetter("PointWithCustomGetter")
+                        }, "sc_arg_props#2");
+                    }
+                });
+        }
+
+        [Fact]
+        public async Task EvaluateExpressions() => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+            "run",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "d + e");
+                CheckContentValue(evaluate, "203");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "e + 10");
+                CheckContentValue(evaluate, "112");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "a + a");
+                CheckContentValue(evaluate, "2");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.a + this.b");
+                CheckContentValue(evaluate, "3");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "\"test\" + \"test\"");
+                CheckContentValue(evaluate, "testtest");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "5 + 5");
+                CheckContentValue(evaluate, "10");
+            });
+
+        [Fact]
+        public async Task EvaluateThisExpressions() => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-evaluate-test.cs", 25, 16,
+            "run",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EvaluateTestsClass:EvaluateLocals'); })",
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.a");
+                CheckContentValue(evaluate, "1");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.b");
+                CheckContentValue(evaluate, "2");
+                evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value<string>(), "this.c");
+                CheckContentValue(evaluate, "3");
+
+                // FIXME: not supported yet
+                // evaluate = await EvaluateOnCallFrame (pause_location ["callFrames"][0] ["callFrameId"].Value<string> (), "this.dt");
+                // await CheckDateTimeValue (evaluate, new DateTime (2000, 5, 4, 3, 2, 1));
+            });
+    }
+
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/InspectorClient.cs b/src/mono/wasm/debugger/DebuggerTestSuite/InspectorClient.cs
new file mode 100644 (file)
index 0000000..d62bfc2
--- /dev/null
@@ -0,0 +1,81 @@
+// 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.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.WebAssembly.Diagnostics
+{
+    internal class InspectorClient : DevToolsClient
+    {
+        List < (int, TaskCompletionSource<Result>) > pending_cmds = new List < (int, TaskCompletionSource<Result>) > ();
+        Func<string, JObject, CancellationToken, Task> onEvent;
+        int next_cmd_id;
+
+        public InspectorClient(ILogger logger) : base(logger) { }
+
+        Task HandleMessage(string msg, CancellationToken token)
+        {
+            var res = JObject.Parse(msg);
+            if (res["id"] == null)
+                DumpProtocol(string.Format("Event method: {0} params: {1}", res["method"], res["params"]));
+            else
+                DumpProtocol(string.Format("Response id: {0} res: {1}", res["id"], res));
+
+            if (res["id"] == null)
+                return onEvent(res["method"].Value<string>(), res["params"] as JObject, token);
+            var id = res["id"].Value<int>();
+            var idx = pending_cmds.FindIndex(e => e.Item1 == id);
+            var item = pending_cmds[idx];
+            pending_cmds.RemoveAt(idx);
+            item.Item2.SetResult(Result.FromJson(res));
+            return null;
+        }
+
+        public async Task Connect(
+            Uri uri,
+            Func<string, JObject, CancellationToken, Task> onEvent,
+            Func<CancellationToken, Task> send,
+            CancellationToken token)
+        {
+
+            this.onEvent = onEvent;
+            await ConnectWithMainLoops(uri, HandleMessage, send, token);
+        }
+
+        public Task<Result> SendCommand(string method, JObject args, CancellationToken token)
+        {
+            int id = ++next_cmd_id;
+            if (args == null)
+                args = new JObject();
+
+            var o = JObject.FromObject(new
+            {
+                id = id,
+                    method = method,
+                    @params = args
+            });
+
+            var tcs = new TaskCompletionSource<Result>();
+            pending_cmds.Add((id, tcs));
+
+            var str = o.ToString();
+            //Log ("protocol", $"SendCommand: id: {id} method: {method} params: {args}");
+
+            var bytes = Encoding.UTF8.GetBytes(str);
+            Send(bytes, token);
+            return tcs.Task;
+        }
+
+        protected virtual void DumpProtocol(string msg)
+        {
+            // Console.WriteLine (msg);
+            //XXX make logging not stupid
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs
new file mode 100644 (file)
index 0000000..e121870
--- /dev/null
@@ -0,0 +1,560 @@
+// 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 Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+
+    public class PointerTests : DebuggerTestBase
+    {
+
+        public static TheoryData<string, string, string, int, string, bool> PointersTestData =>
+            new TheoryData<string, string, string, int, string, bool>
+            { { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "LocalPointers", 32, "LocalPointers", false },
+                { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "LocalPointers", 32, "LocalPointers", true },
+                { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "MoveNext", false },
+                { $"invoke_static_method_async ('[debugger-test] DebuggerTests.PointerTests:LocalPointersAsync');", "DebuggerTests.PointerTests", "LocalPointersAsync", 32, "MoveNext", true }
+            };
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalPointersToPrimitiveTypes(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    ip = TPointer("int*"),
+                        ip_null = TPointer("int*", is_null : true),
+                        ipp = TPointer("int**"),
+                        ipp_null = TPointer("int**"),
+
+                        cvalue0 = TSymbol("113 'q'"),
+                        cp = TPointer("char*"),
+
+                        vp = TPointer("void*"),
+                        vp_null = TPointer("void*", is_null : true),
+                }, "locals", num_fields : 26);
+
+                var props = await GetObjectOnLocals(locals, "ip");
+                await CheckPointerValue(props, "*ip", TNumber(5), "locals");
+
+                {
+                    var ipp_props = await GetObjectOnLocals(locals, "ipp");
+                    await CheckPointerValue(ipp_props, "*ipp", TPointer("int*"));
+
+                    ipp_props = await GetObjectOnLocals(ipp_props, "*ipp");
+                    await CheckPointerValue(ipp_props, "**ipp", TNumber(5));
+                }
+
+                {
+                    var ipp_props = await GetObjectOnLocals(locals, "ipp_null");
+                    await CheckPointerValue(ipp_props, "*ipp_null", TPointer("int*", is_null : true));
+                }
+
+                // *cp
+                props = await GetObjectOnLocals(locals, "cp");
+                await CheckPointerValue(props, "*cp", TSymbol("113 'q'"));
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalPointerArrays(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    ipa = TArray("int*[]", 3)
+                }, "locals", num_fields : 26);
+
+                var ipa_elems = await CompareObjectPropertiesFor(locals, "ipa", new []
+                {
+                    TPointer("int*"),
+                        TPointer("int*"),
+                        TPointer("int*", is_null : true)
+                });
+
+                await CheckArrayElements(ipa_elems, new []
+                {
+                    TNumber(5),
+                        TNumber(10),
+                        null
+                });
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalDoublePointerToPrimitiveTypeArrays(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    ippa = TArray("int**[]", 5)
+                }, "locals", num_fields : 26);
+
+                var ippa_elems = await CompareObjectPropertiesFor(locals, "ippa", new []
+                {
+                    TPointer("int**"),
+                        TPointer("int**"),
+                        TPointer("int**"),
+                        TPointer("int**"),
+                        TPointer("int**", is_null : true)
+                });
+
+                {
+                    var actual_elems = await CheckArrayElements(ippa_elems, new []
+                    {
+                        TPointer("int*"),
+                            TPointer("int*", is_null : true),
+                            TPointer("int*"),
+                            TPointer("int*", is_null : true),
+                            null
+                    });
+
+                    var val = await GetObjectOnLocals(actual_elems[0], "*[0]");
+                    await CheckPointerValue(val, "**[0]", TNumber(5));
+
+                    val = await GetObjectOnLocals(actual_elems[2], "*[2]");
+                    await CheckPointerValue(val, "**[2]", TNumber(5));
+                }
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalPointersToValueTypes(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    dt = TValueType("System.DateTime", dt.ToString()),
+                        dtp = TPointer("System.DateTime*"),
+                        dtp_null = TPointer("System.DateTime*", is_null : true),
+
+                        gsp = TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*"),
+                        gsp_null = TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*")
+                }, "locals", num_fields : 26);
+
+                await CheckDateTime(locals, "dt", dt);
+
+                // *dtp
+                var props = await GetObjectOnLocals(locals, "dtp");
+                await CheckDateTime(props, "*dtp", dt);
+
+                var gsp_props = await GetObjectOnLocals(locals, "gsp");
+                await CheckPointerValue(gsp_props, "*gsp", TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>"), "locals#gsp");
+
+                {
+                    var gs_dt = new DateTime(1, 2, 3, 4, 5, 6);
+
+                    var gsp_deref_props = await GetObjectOnLocals(gsp_props, "*gsp");
+                    await CheckProps(gsp_deref_props, new
+                    {
+                        Value = TValueType("System.DateTime", gs_dt.ToString()),
+                            IntField = TNumber(4),
+                            DTPP = TPointer("System.DateTime**")
+                    }, "locals#gsp#deref");
+                    {
+                        var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+                        await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*"), "locals#*gsp");
+
+                        var dtpp_deref_props = await GetObjectOnLocals(dtpp_props, "*DTPP");
+                        await CheckDateTime(dtpp_deref_props, "**DTPP", dt);
+                    }
+                }
+
+                // gsp_null
+                var gsp_w_n_props = await GetObjectOnLocals(locals, "gsp_null");
+                await CheckPointerValue(gsp_w_n_props, "*gsp_null", TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>"), "locals#gsp");
+
+                {
+                    var gs_dt = new DateTime(1, 2, 3, 4, 5, 6);
+
+                    var gsp_deref_props = await GetObjectOnLocals(gsp_w_n_props, "*gsp_null");
+                    await CheckProps(gsp_deref_props, new
+                    {
+                        Value = TValueType("System.DateTime", gs_dt.ToString()),
+                            IntField = TNumber(4),
+                            DTPP = TPointer("System.DateTime**")
+                    }, "locals#gsp#deref");
+                    {
+                        var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+                        await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*", is_null : true), "locals#*gsp");
+                    }
+                }
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalPointersToValueTypeArrays(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    dtpa = TArray("System.DateTime*[]", 2)
+                }, "locals", num_fields : 26);
+
+                // dtpa
+                var dtpa_elems = (await CompareObjectPropertiesFor(locals, "dtpa", new []
+                {
+                    TPointer("System.DateTime*"),
+                        TPointer("System.DateTime*", is_null : true)
+                }));
+                {
+                    var actual_elems = await CheckArrayElements(dtpa_elems, new []
+                    {
+                        TValueType("System.DateTime", dt.ToString()),
+                            null
+                    });
+
+                    await CheckDateTime(actual_elems[0], "*[0]", dt);
+                }
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalPointersToGenericValueTypeArrays(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    gspa = TArray("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*[]", 3),
+                }, "locals", num_fields : 26);
+
+                // dtpa
+                var gspa_elems = await CompareObjectPropertiesFor(locals, "gspa", new []
+                {
+                    TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*", is_null : true),
+                        TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*"),
+                        TPointer("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>*"),
+                });
+                {
+                    var gs_dt = new DateTime(1, 2, 3, 4, 5, 6);
+                    var actual_elems = await CheckArrayElements(gspa_elems, new []
+                    {
+                        null,
+                        TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>"),
+                        TValueType("DebuggerTests.GenericStructWithUnmanagedT<System.DateTime>")
+                    });
+
+                    // *[1]
+                    {
+                        var gsp_deref_props = await GetObjectOnLocals(actual_elems[1], "*[1]");
+                        await CheckProps(gsp_deref_props, new
+                        {
+                            Value = TValueType("System.DateTime", gs_dt.ToString()),
+                                IntField = TNumber(4),
+                                DTPP = TPointer("System.DateTime**")
+                        }, "locals#gsp#deref");
+                        {
+                            var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+                            await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*"), "locals#*gsp");
+
+                            dtpp_props = await GetObjectOnLocals(dtpp_props, "*DTPP");
+                            await CheckDateTime(dtpp_props, "**DTPP", dt);
+                        }
+                    }
+
+                    // *[2]
+                    {
+                        var gsp_deref_props = await GetObjectOnLocals(actual_elems[2], "*[2]");
+                        await CheckProps(gsp_deref_props, new
+                        {
+                            Value = TValueType("System.DateTime", gs_dt.ToString()),
+                                IntField = TNumber(4),
+                                DTPP = TPointer("System.DateTime**")
+                        }, "locals#gsp#deref");
+                        {
+                            var dtpp_props = await GetObjectOnLocals(gsp_deref_props, "DTPP");
+                            await CheckPointerValue(dtpp_props, "*DTPP", TPointer("System.DateTime*", is_null : true), "locals#*gsp");
+                        }
+                    }
+                }
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalDoublePointersToValueTypeArrays(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    dtppa = TArray("System.DateTime**[]", 3),
+                }, "locals", num_fields : 26);
+
+                // DateTime**[] dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+                var dtppa_elems = (await CompareObjectPropertiesFor(locals, "dtppa", new []
+                {
+                    TPointer("System.DateTime**"),
+                        TPointer("System.DateTime**"),
+                        TPointer("System.DateTime**", is_null : true)
+                }));
+
+                var exp_elems = new []
+                {
+                    TPointer("System.DateTime*"),
+                    TPointer("System.DateTime*", is_null : true),
+                    null
+                };
+
+                var actual_elems = new JToken[exp_elems.Length];
+                for (int i = 0; i < exp_elems.Length; i++)
+                {
+                    if (exp_elems[i] != null)
+                    {
+                        actual_elems[i] = await GetObjectOnLocals(dtppa_elems, i.ToString());
+                        await CheckPointerValue(actual_elems[i], $"*[{i}]", exp_elems[i], $"dtppa->");
+                    }
+                }
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersTestData))]
+        public async Task InspectLocalPointersInClasses(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    cwp = TObject("DebuggerTests.GenericClassWithPointers<System.DateTime>"),
+                        cwp_null = TObject("DebuggerTests.GenericClassWithPointers<System.DateTime>")
+                }, "locals", num_fields : 26);
+
+                var cwp_props = await GetObjectOnLocals(locals, "cwp");
+                var ptr_props = await GetObjectOnLocals(cwp_props, "Ptr");
+                await CheckDateTime(ptr_props, "*Ptr", dt);
+            });
+
+        public static TheoryData<string, string, string, int, string, bool> PointersAsMethodArgsTestData =>
+            new TheoryData<string, string, string, int, string, bool>
+            { { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "PointersAsArgsTest", 2, "PointersAsArgsTest", false },
+                { $"invoke_static_method ('[debugger-test] DebuggerTests.PointerTests:LocalPointers');", "DebuggerTests.PointerTests", "PointersAsArgsTest", 2, "PointersAsArgsTest", true },
+            };
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersAsMethodArgsTestData))]
+        public async Task InspectPrimitiveTypePointersAsMethodArgs(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    ip = TPointer("int*"),
+                        ipp = TPointer("int**"),
+                        ipa = TArray("int*[]", 3),
+                        ippa = TArray("int**[]", 5)
+                }, "locals", num_fields : 8);
+
+                // ip
+                var props = await GetObjectOnLocals(locals, "ip");
+                await CheckPointerValue(props, "*ip", TNumber(5), "locals");
+
+                // ipp
+                var ipp_props = await GetObjectOnLocals(locals, "ipp");
+                await CheckPointerValue(ipp_props, "*ipp", TPointer("int*"));
+
+                ipp_props = await GetObjectOnLocals(ipp_props, "*ipp");
+                await CheckPointerValue(ipp_props, "**ipp", TNumber(5));
+
+                // ipa
+                var ipa_elems = await CompareObjectPropertiesFor(locals, "ipa", new []
+                {
+                    TPointer("int*"),
+                        TPointer("int*"),
+                        TPointer("int*", is_null : true)
+                });
+
+                await CheckArrayElements(ipa_elems, new []
+                {
+                    TNumber(5),
+                        TNumber(10),
+                        null
+                });
+
+                // ippa
+                var ippa_elems = await CompareObjectPropertiesFor(locals, "ippa", new []
+                {
+                    TPointer("int**"),
+                        TPointer("int**"),
+                        TPointer("int**"),
+                        TPointer("int**"),
+                        TPointer("int**", is_null : true)
+                });
+
+                {
+                    var actual_elems = await CheckArrayElements(ippa_elems, new []
+                    {
+                        TPointer("int*"),
+                            TPointer("int*", is_null : true),
+                            TPointer("int*"),
+                            TPointer("int*", is_null : true),
+                            null
+                    });
+
+                    var val = await GetObjectOnLocals(actual_elems[0], "*[0]");
+                    await CheckPointerValue(val, "**[0]", TNumber(5));
+
+                    val = await GetObjectOnLocals(actual_elems[2], "*[2]");
+                    await CheckPointerValue(val, "**[2]", TNumber(5));
+                }
+            });
+
+        [Theory]
+        [MemberDataAttribute(nameof(PointersAsMethodArgsTestData))]
+        public async Task InspectValueTypePointersAsMethodArgs(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                var dt = new DateTime(5, 6, 7, 8, 9, 10);
+                await CheckProps(locals, new
+                {
+                    dtp = TPointer("System.DateTime*"),
+                        dtpp = TPointer("System.DateTime**"),
+                        dtpa = TArray("System.DateTime*[]", 2),
+                        dtppa = TArray("System.DateTime**[]", 3)
+                }, "locals", num_fields : 8);
+
+                // *dtp
+                var dtp_props = await GetObjectOnLocals(locals, "dtp");
+                await CheckDateTime(dtp_props, "*dtp", dt);
+
+                // *dtpp
+                var dtpp_props = await GetObjectOnLocals(locals, "dtpp");
+                await CheckPointerValue(dtpp_props, "*dtpp", TPointer("System.DateTime*"), "locals");
+
+                dtpp_props = await GetObjectOnLocals(dtpp_props, "*dtpp");
+                await CheckDateTime(dtpp_props, "**dtpp", dt);
+
+                // dtpa
+                var dtpa_elems = (await CompareObjectPropertiesFor(locals, "dtpa", new []
+                {
+                    TPointer("System.DateTime*"),
+                        TPointer("System.DateTime*", is_null : true)
+                }));
+                {
+                    var actual_elems = await CheckArrayElements(dtpa_elems, new []
+                    {
+                        TValueType("System.DateTime", dt.ToString()),
+                            null
+                    });
+
+                    await CheckDateTime(actual_elems[0], "*[0]", dt);
+                }
+
+                // dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+                var dtppa_elems = (await CompareObjectPropertiesFor(locals, "dtppa", new []
+                {
+                    TPointer("System.DateTime**"),
+                        TPointer("System.DateTime**"),
+                        TPointer("System.DateTime**", is_null : true)
+                }));
+
+                var exp_elems = new []
+                {
+                    TPointer("System.DateTime*"),
+                    TPointer("System.DateTime*", is_null : true),
+                    null
+                };
+
+                await CheckArrayElements(dtppa_elems, exp_elems);
+            });
+
+        [Theory]
+        [InlineData("invoke_static_method ('[debugger-test] Math:UseComplex', 0, 0);", "Math", "UseComplex", 3, "UseComplex", false)]
+        [InlineData("invoke_static_method ('[debugger-test] Math:UseComplex', 0, 0);", "Math", "UseComplex", 3, "UseComplex", true)]
+        public async Task DerefNonPointerObject(string eval_fn, string type, string method, int line_offset, string bp_function_name, bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            type, method, line_offset, bp_function_name,
+            "window.setTimeout(function() { " + eval_fn + " })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+
+                // this will generate the object ids
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                var complex = GetAndAssertObjectWithName(locals, "complex");
+
+                // try to deref the non-pointer object, as a pointer
+                var props = await GetProperties(complex["value"]["objectId"].Value<string>().Replace(":object:", ":pointer:"));
+                Assert.Empty(props.Values());
+
+                // try to deref an invalid pointer id
+                props = await GetProperties("dotnet:pointer:123897");
+                Assert.Empty(props.Values());
+            });
+
+        async Task<JToken[]> CheckArrayElements(JToken array, JToken[] exp_elems)
+        {
+            var actual_elems = new JToken[exp_elems.Length];
+            for (int i = 0; i < exp_elems.Length; i++)
+            {
+                if (exp_elems[i] != null)
+                {
+                    actual_elems[i] = await GetObjectOnLocals(array, i.ToString());
+                    await CheckPointerValue(actual_elems[i], $"*[{i}]", exp_elems[i], $"dtppa->");
+                }
+            }
+
+            return actual_elems;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs
new file mode 100644 (file)
index 0000000..b038d2c
--- /dev/null
@@ -0,0 +1,990 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.WebSockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+namespace DebuggerTests
+{
+    class Inspector
+    {
+        // InspectorClient client;
+        Dictionary<string, TaskCompletionSource<JObject>> notifications = new Dictionary<string, TaskCompletionSource<JObject>>();
+        Dictionary<string, Func<JObject, CancellationToken, Task>> eventListeners = new Dictionary<string, Func<JObject, CancellationToken, Task>>();
+
+        public const string PAUSE = "pause";
+        public const string READY = "ready";
+
+        public Task<JObject> WaitFor(string what)
+        {
+            if (notifications.ContainsKey(what))
+                throw new Exception($"Invalid internal state, waiting for {what} while another wait is already setup");
+            var n = new TaskCompletionSource<JObject>();
+            notifications[what] = n;
+            return n.Task;
+        }
+
+        void NotifyOf(string what, JObject args)
+        {
+            if (!notifications.ContainsKey(what))
+                throw new Exception($"Invalid internal state, notifying of {what}, but nobody waiting");
+            notifications[what].SetResult(args);
+            notifications.Remove(what);
+        }
+
+        public void On(string evtName, Func<JObject, CancellationToken, Task> cb)
+        {
+            eventListeners[evtName] = cb;
+        }
+
+        void FailAllWaitersWithException(JObject exception)
+        {
+            foreach (var tcs in notifications.Values)
+                tcs.SetException(new ArgumentException(exception.ToString()));
+        }
+
+        async Task OnMessage(string method, JObject args, CancellationToken token)
+        {
+            //System.Console.WriteLine("OnMessage " + method + args);
+            switch (method)
+            {
+                case "Debugger.paused":
+                    NotifyOf(PAUSE, args);
+                    break;
+                case "Mono.runtimeReady":
+                    NotifyOf(READY, args);
+                    break;
+                case "Runtime.consoleAPICalled":
+                    Console.WriteLine("CWL: {0}", args?["args"] ? [0] ? ["value"]);
+                    break;
+            }
+            if (eventListeners.ContainsKey(method))
+                await eventListeners[method](args, token);
+            else if (String.Compare(method, "Runtime.exceptionThrown") == 0)
+                FailAllWaitersWithException(args);
+        }
+
+        public async Task Ready(Func<InspectorClient, CancellationToken, Task> cb = null, TimeSpan? span = null)
+        {
+            using(var cts = new CancellationTokenSource())
+            {
+                cts.CancelAfter(span?.Milliseconds ?? 60 * 1000); //tests have 1 minute to complete by default
+                var uri = new Uri($"ws://{TestHarnessProxy.Endpoint.Authority}/launch-chrome-and-connect");
+                using var loggerFactory = LoggerFactory.Create(
+                    builder => builder.AddConsole().AddFilter(null, LogLevel.Information));
+                using(var client = new InspectorClient(loggerFactory.CreateLogger<Inspector>()))
+                {
+                    await client.Connect(uri, OnMessage, async token =>
+                    {
+                    Task[] init_cmds = {
+                    client.SendCommand("Profiler.enable", null, token),
+                    client.SendCommand("Runtime.enable", null, token),
+                    client.SendCommand("Debugger.enable", null, token),
+                    client.SendCommand("Runtime.runIfWaitingForDebugger", null, token),
+                    WaitFor(READY),
+                        };
+                        // await Task.WhenAll (init_cmds);
+                        Console.WriteLine("waiting for the runtime to be ready");
+                        await init_cmds[4];
+                        Console.WriteLine("runtime ready, TEST TIME");
+                        if (cb != null)
+                        {
+                            Console.WriteLine("await cb(client, token)");
+                            await cb(client, token);
+                        }
+
+                    }, cts.Token);
+                    await client.Close(cts.Token);
+                }
+            }
+        }
+    }
+
+    public class DebuggerTestBase
+    {
+        protected Task startTask;
+
+        static string FindTestPath()
+        {
+            //FIXME how would I locate it otherwise?
+            var test_path = Environment.GetEnvironmentVariable("TEST_SUITE_PATH");
+            //Lets try to guest
+            if (test_path != null && Directory.Exists(test_path))
+                return test_path;
+
+            var cwd = Environment.CurrentDirectory;
+            Console.WriteLine("guessing from {0}", cwd);
+            //tests run from DebuggerTestSuite/bin/Debug/netcoreapp2.1
+            var new_path = Path.Combine(cwd, "../../../../bin/debugger-test-suite");
+            if (File.Exists(Path.Combine(new_path, "debugger-driver.html")))
+                return new_path;
+
+            throw new Exception("Missing TEST_SUITE_PATH env var and could not guess path from CWD");
+        }
+
+        static string[] PROBE_LIST = {
+            "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+            "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
+            "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
+            "/usr/bin/chromium",
+            "/usr/bin/chromium-browser",
+        };
+        static string chrome_path;
+
+        static string FindChromePath()
+        {
+            if (chrome_path != null)
+                return chrome_path;
+
+            foreach (var s in PROBE_LIST)
+            {
+                if (File.Exists(s))
+                {
+                    chrome_path = s;
+                    Console.WriteLine($"Using chrome path: ${s}");
+                    return s;
+                }
+            }
+            throw new Exception("Could not find an installed Chrome to use");
+        }
+
+        public DebuggerTestBase(string driver = "debugger-driver.html")
+        {
+            startTask = TestHarnessProxy.Start(FindChromePath(), FindTestPath(), driver);
+        }
+
+        public Task Ready() => startTask;
+
+        internal DebugTestContext ctx;
+        internal Dictionary<string, string> dicScriptsIdToUrl;
+        internal Dictionary<string, string> dicFileToUrl;
+        internal Dictionary<string, string> SubscribeToScripts(Inspector insp)
+        {
+            dicScriptsIdToUrl = new Dictionary<string, string>();
+            dicFileToUrl = new Dictionary<string, string>();
+            insp.On("Debugger.scriptParsed", async(args, c) =>
+            {
+                var script_id = args?["scriptId"]?.Value<string>();
+                var url = args["url"]?.Value<string>();
+                if (script_id.StartsWith("dotnet://"))
+                {
+                    var dbgUrl = args["dotNetUrl"]?.Value<string>();
+                    var arrStr = dbgUrl.Split("/");
+                    dbgUrl = arrStr[0] + "/" + arrStr[1] + "/" + arrStr[2] + "/" + arrStr[arrStr.Length - 1];
+                    dicScriptsIdToUrl[script_id] = dbgUrl;
+                    dicFileToUrl[dbgUrl] = args["url"]?.Value<string>();
+                }
+                else if (!String.IsNullOrEmpty(url))
+                {
+                    dicFileToUrl[new Uri(url).AbsolutePath] = url;
+                }
+                await Task.FromResult(0);
+            });
+            return dicScriptsIdToUrl;
+        }
+
+        internal async Task CheckInspectLocalsAtBreakpointSite(string url_key, int line, int column, string function_name, string eval_expression,
+            Action<JToken> test_fn = null, Func<JObject, Task> wait_for_event_fn = null, bool use_cfo = false)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+                var bp = await SetBreakpoint(url_key, line, column);
+
+                await EvaluateAndCheck(
+                    eval_expression, url_key, line, column,
+                    function_name,
+                    wait_for_event_fn : async(pause_location) =>
+                    {
+                        //make sure we're on the right bp
+
+                        Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+                        var top_frame = pause_location["callFrames"][0];
+
+                        var scope = top_frame["scopeChain"][0];
+                        Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+                        if (wait_for_event_fn != null)
+                            await wait_for_event_fn(pause_location);
+                        else
+                            await Task.CompletedTask;
+                    },
+                    locals_fn: (locals) =>
+                    {
+                        if (test_fn != null)
+                            test_fn(locals);
+                    }
+                );
+            });
+        }
+
+        // sets breakpoint by method name and line offset
+        internal async Task CheckInspectLocalsAtBreakpointSite(string type, string method, int line_offset, string bp_function_name, string eval_expression,
+            Action<JToken> locals_fn = null, Func<JObject, Task> wait_for_event_fn = null, bool use_cfo = false, string assembly = "debugger-test.dll", int col = 0)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+                var bp = await SetBreakpointInMethod(assembly, type, method, line_offset, col);
+
+                var args = JObject.FromObject(new { expression = eval_expression });
+                var res = await ctx.cli.SendCommand("Runtime.evaluate", args, ctx.token);
+                if (!res.IsOk)
+                {
+                    Console.WriteLine($"Failed to run command {method} with args: {args?.ToString ()}\nresult: {res.Error.ToString ()}");
+                    Assert.True(false, $"SendCommand for {method} failed with {res.Error.ToString ()}");
+                }
+
+                var pause_location = await ctx.insp.WaitFor(Inspector.PAUSE);
+
+                if (bp_function_name != null)
+                    Assert.Equal(bp_function_name, pause_location["callFrames"] ? [0] ? ["functionName"]?.Value<string>());
+
+                Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+                var top_frame = pause_location["callFrames"][0];
+
+                var scope = top_frame["scopeChain"][0];
+                Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+
+                if (wait_for_event_fn != null)
+                    await wait_for_event_fn(pause_location);
+
+                if (locals_fn != null)
+                {
+                    var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                    locals_fn(locals);
+                }
+            });
+        }
+
+        internal void CheckLocation(string script_loc, int line, int column, Dictionary<string, string> scripts, JToken location)
+        {
+            var loc_str = $"{ scripts[location["scriptId"].Value<string>()] }" +
+                $"#{ location ["lineNumber"].Value<int> () }" +
+                $"#{ location ["columnNumber"].Value<int> () }";
+
+            var expected_loc_str = $"{script_loc}#{line}#{column}";
+            Assert.Equal(expected_loc_str, loc_str);
+        }
+
+        internal void CheckNumber<T>(JToken locals, string name, T value)
+        {
+            foreach (var l in locals)
+            {
+                if (name != l["name"]?.Value<string>())
+                    continue;
+                var val = l["value"];
+                Assert.Equal("number", val["type"]?.Value<string>());
+                Assert.Equal(value, val["value"].Value<T>());
+                return;
+            }
+            Assert.True(false, $"Could not find variable '{name}'");
+        }
+
+        internal void CheckString(JToken locals, string name, string value)
+        {
+            foreach (var l in locals)
+            {
+                if (name != l["name"]?.Value<string>())
+                    continue;
+                var val = l["value"];
+                if (value == null)
+                {
+                    Assert.Equal("object", val["type"]?.Value<string>());
+                    Assert.Equal("null", val["subtype"]?.Value<string>());
+                }
+                else
+                {
+                    Assert.Equal("string", val["type"]?.Value<string>());
+                    Assert.Equal(value, val["value"]?.Value<string>());
+                }
+                return;
+            }
+            Assert.True(false, $"Could not find variable '{name}'");
+        }
+
+        internal JToken CheckSymbol(JToken locals, string name, string value)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            var val = l["value"];
+            Assert.Equal("symbol", val["type"]?.Value<string>());
+            Assert.Equal(value, val["value"]?.Value<string>());
+            return l;
+        }
+
+        internal JToken CheckObject(JToken locals, string name, string class_name, string subtype = null, bool is_null = false)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            var val = l["value"];
+            Assert.Equal("object", val["type"]?.Value<string>());
+            Assert.True(val["isValueType"] == null || !val["isValueType"].Value<bool>());
+            Assert.Equal(class_name, val["className"]?.Value<string>());
+
+            var has_null_subtype = val["subtype"] != null && val["subtype"]?.Value<string>() == "null";
+            Assert.Equal(is_null, has_null_subtype);
+            if (subtype != null)
+                Assert.Equal(subtype, val["subtype"]?.Value<string>());
+
+            return l;
+        }
+
+        internal async Task<JToken> CheckPointerValue(JToken locals, string name, JToken expected, string label = null)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            await CheckValue(l["value"], expected, $"{label ?? String.Empty}-{name}");
+            return l;
+        }
+
+        internal async Task CheckDateTime(JToken locals, string name, DateTime expected)
+        {
+            var obj = GetAndAssertObjectWithName(locals, name);
+            await CheckDateTimeValue(obj["value"], expected);
+        }
+
+        internal async Task CheckDateTimeValue(JToken value, DateTime expected)
+        {
+            AssertEqual("System.DateTime", value["className"]?.Value<string>(), "className");
+            AssertEqual(expected.ToString(), value["description"]?.Value<string>(), "description");
+
+            var members = await GetProperties(value["objectId"]?.Value<string>());
+
+            // not checking everything
+            CheckNumber(members, "Year", expected.Year);
+            CheckNumber(members, "Month", expected.Month);
+            CheckNumber(members, "Day", expected.Day);
+            CheckNumber(members, "Hour", expected.Hour);
+            CheckNumber(members, "Minute", expected.Minute);
+            CheckNumber(members, "Second", expected.Second);
+
+            // FIXME: check some float properties too
+        }
+
+        internal JToken CheckBool(JToken locals, string name, bool expected)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            var val = l["value"];
+            Assert.Equal("boolean", val["type"]?.Value<string>());
+            if (val["value"] == null)
+                Assert.True(false, "expected bool value not found for variable named {name}");
+            Assert.Equal(expected, val["value"]?.Value<bool>());
+
+            return l;
+        }
+
+        internal void CheckContentValue(JToken token, string value)
+        {
+            var val = token["value"].Value<string>();
+            Assert.Equal(value, val);
+        }
+
+        internal JToken CheckValueType(JToken locals, string name, string class_name)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            var val = l["value"];
+            Assert.Equal("object", val["type"]?.Value<string>());
+            Assert.True(val["isValueType"] != null && val["isValueType"].Value<bool>());
+            Assert.Equal(class_name, val["className"]?.Value<string>());
+            return l;
+        }
+
+        internal JToken CheckEnum(JToken locals, string name, string class_name, string descr)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            var val = l["value"];
+            Assert.Equal("object", val["type"]?.Value<string>());
+            Assert.True(val["isEnum"] != null && val["isEnum"].Value<bool>());
+            Assert.Equal(class_name, val["className"]?.Value<string>());
+            Assert.Equal(descr, val["description"]?.Value<string>());
+            return l;
+        }
+
+        internal void CheckArray(JToken locals, string name, string class_name)
+        {
+            foreach (var l in locals)
+            {
+                if (name != l["name"]?.Value<string>())
+                    continue;
+
+                var val = l["value"];
+                Assert.Equal("object", val["type"]?.Value<string>());
+                Assert.Equal("array", val["subtype"]?.Value<string>());
+                Assert.Equal(class_name, val["className"]?.Value<string>());
+
+                //FIXME: elements?
+                return;
+            }
+            Assert.True(false, $"Could not find variable '{name}'");
+        }
+
+        internal JToken GetAndAssertObjectWithName(JToken obj, string name)
+        {
+            var l = obj.FirstOrDefault(jt => jt["name"]?.Value<string>() == name);
+            if (l == null)
+                Assert.True(false, $"Could not find variable '{name}'");
+            return l;
+        }
+
+        internal async Task<Result> SendCommand(string method, JObject args)
+        {
+            var res = await ctx.cli.SendCommand(method, args, ctx.token);
+            if (!res.IsOk)
+            {
+                Console.WriteLine($"Failed to run command {method} with args: {args?.ToString ()}\nresult: {res.Error.ToString ()}");
+                Assert.True(false, $"SendCommand for {method} failed with {res.Error.ToString ()}");
+            }
+            return res;
+        }
+
+        internal async Task<Result> Evaluate(string expression)
+        {
+            return await SendCommand("Runtime.evaluate", JObject.FromObject(new { expression = expression }));
+        }
+
+        internal void AssertLocation(JObject args, string methodName)
+        {
+            Assert.Equal(methodName, args["callFrames"] ? [0] ? ["functionName"]?.Value<string>());
+        }
+
+        // Place a breakpoint in the given method and run until its hit
+        // Return the Debugger.paused data
+        internal async Task<JObject> RunUntil(string methodName)
+        {
+            await SetBreakpointInMethod("debugger-test", "DebuggerTest", methodName);
+            // This will run all the tests until it hits the bp
+            await Evaluate("window.setTimeout(function() { invoke_run_all (); }, 1);");
+            var wait_res = await ctx.insp.WaitFor(Inspector.PAUSE);
+            AssertLocation(wait_res, "locals_inner");
+            return wait_res;
+        }
+
+        internal async Task<JObject> StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name,
+            Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null, int times = 1)
+        {
+            for (int i = 0; i < times - 1; i++)
+            {
+                await SendCommandAndCheck(null, $"Debugger.step{kind.ToString ()}", null, -1, -1, null);
+            }
+
+            // Check for method/line etc only at the last step
+            return await SendCommandAndCheck(
+                null, $"Debugger.step{kind.ToString ()}", script_loc, line, column, function_name,
+                wait_for_event_fn : wait_for_event_fn,
+                locals_fn : locals_fn);
+        }
+
+        internal async Task<JObject> EvaluateAndCheck(string expression, string script_loc, int line, int column, string function_name,
+            Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null) => await SendCommandAndCheck(
+            JObject.FromObject(new { expression = expression }),
+            "Runtime.evaluate", script_loc, line, column, function_name,
+            wait_for_event_fn : wait_for_event_fn,
+            locals_fn : locals_fn);
+
+        internal async Task<JObject> SendCommandAndCheck(JObject args, string method, string script_loc, int line, int column, string function_name,
+            Func<JObject, Task> wait_for_event_fn = null, Action<JToken> locals_fn = null, string waitForEvent = Inspector.PAUSE)
+        {
+            var res = await ctx.cli.SendCommand(method, args, ctx.token);
+            if (!res.IsOk)
+            {
+                Console.WriteLine($"Failed to run command {method} with args: {args?.ToString ()}\nresult: {res.Error.ToString ()}");
+                Assert.True(false, $"SendCommand for {method} failed with {res.Error.ToString ()}");
+            }
+
+            var wait_res = await ctx.insp.WaitFor(waitForEvent);
+
+            if (function_name != null)
+                Assert.Equal(function_name, wait_res["callFrames"] ? [0] ? ["functionName"]?.Value<string>());
+
+            if (script_loc != null)
+                CheckLocation(script_loc, line, column, ctx.scripts, wait_res["callFrames"][0]["location"]);
+
+            if (wait_for_event_fn != null)
+                await wait_for_event_fn(wait_res);
+
+            if (locals_fn != null)
+            {
+                var locals = await GetProperties(wait_res["callFrames"][0]["callFrameId"].Value<string>());
+                locals_fn(locals);
+            }
+
+            return wait_res;
+        }
+
+        internal async Task CheckDelegate(JToken locals, string name, string className, string target)
+        {
+            var l = GetAndAssertObjectWithName(locals, name);
+            var val = l["value"];
+
+            await CheckDelegate(l, TDelegate(className, target), name);
+        }
+
+        internal async Task CheckDelegate(JToken actual_val, JToken exp_val, string label)
+        {
+            AssertEqual("object", actual_val["type"]?.Value<string>(), $"{label}-type");
+            AssertEqual(exp_val["className"]?.Value<string>(), actual_val["className"]?.Value<string>(), $"{label}-className");
+
+            var actual_target = actual_val["description"]?.Value<string>();
+            Assert.True(actual_target != null, $"${label}-description");
+            var exp_target = exp_val["target"].Value<string>();
+
+            CheckDelegateTarget(actual_target, exp_target);
+
+            var del_props = await GetProperties(actual_val["objectId"]?.Value<string>());
+            AssertEqual(1, del_props.Count(), $"${label}-delegate-properties-count");
+
+            var obj = del_props.Where(jt => jt["name"]?.Value<string>() == "Target").FirstOrDefault();
+            Assert.True(obj != null, $"[{label}] Property named 'Target' found found in delegate properties");
+
+            AssertEqual("symbol", obj["value"] ? ["type"]?.Value<string>(), $"{label}#Target#type");
+            CheckDelegateTarget(obj["value"] ? ["value"]?.Value<string>(), exp_target);
+
+            return;
+
+            void CheckDelegateTarget(string actual_target, string exp_target)
+            {
+                var parts = exp_target.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
+                if (parts.Length == 1)
+                {
+                    // not a generated method
+                    AssertEqual(exp_target, actual_target, $"{label}-description");
+                }
+                else
+                {
+                    bool prefix = actual_target.StartsWith(parts[0], StringComparison.Ordinal);
+                    Assert.True(prefix, $"{label}-description, Expected target to start with '{parts[0]}'. Actual: '{actual_target}'");
+
+                    var remaining = actual_target.Substring(parts[0].Length);
+                    bool suffix = remaining.EndsWith(parts[1], StringComparison.Ordinal);
+                    Assert.True(prefix, $"{label}-description, Expected target to end with '{parts[1]}'. Actual: '{remaining}'");
+                }
+            }
+        }
+
+        internal async Task CheckCustomType(JToken actual_val, JToken exp_val, string label)
+        {
+            var ctype = exp_val["__custom_type"].Value<string>();
+            switch (ctype)
+            {
+                case "delegate":
+                    await CheckDelegate(actual_val, exp_val, label);
+                    break;
+
+                case "pointer":
+                    {
+
+                        if (exp_val["is_null"]?.Value<bool>() == true)
+                        {
+                            AssertEqual("symbol", actual_val["type"]?.Value<string>(), $"{label}-type");
+
+                            var exp_val_str = $"({exp_val ["type_name"]?.Value<string>()}) 0";
+                            AssertEqual(exp_val_str, actual_val["value"]?.Value<string>(), $"{label}-value");
+                            AssertEqual(exp_val_str, actual_val["description"]?.Value<string>(), $"{label}-description");
+                        }
+                        else if (exp_val["is_void"]?.Value<bool>() == true)
+                        {
+                            AssertEqual("symbol", actual_val["type"]?.Value<string>(), $"{label}-type");
+
+                            var exp_val_str = $"({exp_val ["type_name"]?.Value<string>()})";
+                            AssertStartsWith(exp_val_str, actual_val["value"]?.Value<string>(), $"{label}-value");
+                            AssertStartsWith(exp_val_str, actual_val["description"]?.Value<string>(), $"{label}-description");
+                        }
+                        else
+                        {
+                            AssertEqual("object", actual_val["type"]?.Value<string>(), $"{label}-type");
+
+                            var exp_prefix = $"({exp_val ["type_name"]?.Value<string>()})";
+                            AssertStartsWith(exp_prefix, actual_val["className"]?.Value<string>(), $"{label}-className");
+                            AssertStartsWith(exp_prefix, actual_val["description"]?.Value<string>(), $"{label}-description");
+                            Assert.False(actual_val["className"]?.Value<string>() == $"{exp_prefix} 0", $"[{label}] Expected a non-null value, but got {actual_val}");
+                        }
+                        break;
+                    }
+
+                case "getter":
+                    {
+                        // For getter, `actual_val` is not `.value`, instead it's the container object
+                        // which has a `.get` instead of a `.value`
+                        var get = actual_val["get"];
+                        Assert.True(get != null, $"[{label}] No `get` found. {(actual_val != null ? "Make sure to pass the container object for testing getters, and not the ['value']": String.Empty)}");
+
+                        AssertEqual("Function", get["className"]?.Value<string>(), $"{label}-className");
+                        AssertStartsWith($"get {exp_val ["type_name"]?.Value<string> ()} ()", get["description"]?.Value<string>(), $"{label}-description");
+                        AssertEqual("function", get["type"]?.Value<string>(), $"{label}-type");
+
+                        break;
+                    }
+
+                case "ignore_me":
+                    // nothing to check ;)
+                    break;
+
+                default:
+                    throw new ArgumentException($"{ctype} not supported");
+            }
+        }
+
+        internal async Task CheckProps(JToken actual, object exp_o, string label, int num_fields = -1)
+        {
+            if (exp_o.GetType().IsArray || exp_o is JArray)
+            {
+                if (!(actual is JArray actual_arr))
+                {
+                    Assert.True(false, $"[{label}] Expected to get an array here but got {actual}");
+                    return;
+                }
+
+                var exp_v_arr = JArray.FromObject(exp_o);
+                AssertEqual(exp_v_arr.Count, actual_arr.Count(), $"{label}-count");
+
+                for (int i = 0; i < exp_v_arr.Count; i++)
+                {
+                    var exp_i = exp_v_arr[i];
+                    var act_i = actual_arr[i];
+
+                    AssertEqual(i.ToString(), act_i["name"]?.Value<string>(), $"{label}-[{i}].name");
+                    if (exp_i != null)
+                        await CheckValue(act_i["value"], exp_i, $"{label}-{i}th value");
+                }
+
+                return;
+            }
+
+            // Not an array
+            var exp = exp_o as JObject;
+            if (exp == null)
+                exp = JObject.FromObject(exp_o);
+
+            num_fields = num_fields < 0 ? exp.Values<JToken>().Count() : num_fields;
+            Assert.True(num_fields == actual.Count(), $"[{label}] Number of fields don't match, Expected: {num_fields}, Actual: {actual.Count()}");
+
+            foreach (var kvp in exp)
+            {
+                var exp_name = kvp.Key;
+                var exp_val = kvp.Value;
+
+                var actual_obj = actual.FirstOrDefault(jt => jt["name"]?.Value<string>() == exp_name);
+                if (actual_obj == null)
+                {
+                    Assert.True(actual_obj != null, $"[{label}] Could not find property named '{exp_name}'");
+                }
+
+                Assert.True(actual_obj != null, $"[{label}] not value found for property named '{exp_name}'");
+
+                var actual_val = actual_obj["value"];
+                if (exp_val.Type == JTokenType.Array)
+                {
+                    var actual_props = await GetProperties(actual_val["objectId"]?.Value<string>());
+                    await CheckProps(actual_props, exp_val, $"{label}-{exp_name}");
+                }
+                else if (exp_val["__custom_type"] != null && exp_val["__custom_type"]?.Value<string>() == "getter")
+                {
+                    // hack: for getters, actual won't have a .value
+                    await CheckCustomType(actual_obj, exp_val, $"{label}#{exp_name}");
+                }
+                else
+                {
+                    await CheckValue(actual_val, exp_val, $"{label}#{exp_name}");
+                }
+            }
+        }
+
+        internal async Task CheckValue(JToken actual_val, JToken exp_val, string label)
+        {
+            if (exp_val["__custom_type"] != null)
+            {
+                await CheckCustomType(actual_val, exp_val, label);
+                return;
+            }
+
+            if (exp_val["type"] == null && actual_val["objectId"] != null)
+            {
+                var new_val = await GetProperties(actual_val["objectId"].Value<string>());
+                await CheckProps(new_val, exp_val, $"{label}-{actual_val["objectId"]?.Value<string>()}");
+                return;
+            }
+
+            foreach (var jp in exp_val.Values<JProperty>())
+            {
+                if (jp.Value.Type == JTokenType.Object)
+                {
+                    var new_val = await GetProperties(actual_val["objectId"].Value<string>());
+                    await CheckProps(new_val, jp.Value, $"{label}-{actual_val["objectId"]?.Value<string>()}");
+
+                    continue;
+                }
+
+                var exp_val_str = jp.Value.Value<string>();
+                bool null_or_empty_exp_val = String.IsNullOrEmpty(exp_val_str);
+
+                var actual_field_val = actual_val.Values<JProperty>().FirstOrDefault(a_jp => a_jp.Name == jp.Name);
+                var actual_field_val_str = actual_field_val?.Value?.Value<string>();
+                if (null_or_empty_exp_val && String.IsNullOrEmpty(actual_field_val_str))
+                    continue;
+
+                Assert.True(actual_field_val != null, $"[{label}] Could not find value field named {jp.Name}");
+
+                Assert.True(exp_val_str == actual_field_val_str,
+                    $"[{label}] Value for json property named {jp.Name} didn't match.\n" +
+                    $"Expected: {jp.Value.Value<string> ()}\n" +
+                    $"Actual:   {actual_field_val.Value.Value<string> ()}");
+            }
+        }
+
+        internal async Task<JToken> GetLocalsForFrame(JToken frame, string script_loc, int line, int column, string function_name)
+        {
+            CheckLocation(script_loc, line, column, ctx.scripts, frame["location"]);
+            Assert.Equal(function_name, frame["functionName"].Value<string>());
+
+            return await GetProperties(frame["callFrameId"].Value<string>());
+        }
+
+        internal async Task<JToken> GetObjectOnFrame(JToken frame, string name)
+        {
+            var locals = await GetProperties(frame["callFrameId"].Value<string>());
+            return await GetObjectOnLocals(locals, name);
+        }
+
+        // Find an object with @name, *fetch* the object, and check against @o
+        internal async Task<JToken> CompareObjectPropertiesFor(JToken locals, string name, object o, string label = null, int num_fields = -1)
+        {
+            if (label == null)
+                label = name;
+            var props = await GetObjectOnLocals(locals, name);
+            try
+            {
+                if (o != null)
+                    await CheckProps(props, o, label, num_fields);
+                return props;
+            }
+            catch
+            {
+                throw;
+            }
+        }
+
+        internal async Task<JToken> GetObjectOnLocals(JToken locals, string name)
+        {
+            var obj = GetAndAssertObjectWithName(locals, name);
+            var objectId = obj["value"]["objectId"]?.Value<string>();
+            Assert.True(!String.IsNullOrEmpty(objectId), $"No objectId found for {name}");
+
+            return await GetProperties(objectId);
+        }
+
+        /* @fn_args is for use with `Runtime.callFunctionOn` only */
+        internal async Task<JToken> GetProperties(string id, JToken fn_args = null)
+        {
+            if (ctx.UseCallFunctionOnBeforeGetProperties && !id.StartsWith("dotnet:scope:"))
+            {
+                var fn_decl = "function () { return this; }";
+                var cfo_args = JObject.FromObject(new
+                {
+                    functionDeclaration = fn_decl,
+                        objectId = id
+                });
+                if (fn_args != null)
+                    cfo_args["arguments"] = fn_args;
+
+                var result = await ctx.cli.SendCommand("Runtime.callFunctionOn", cfo_args, ctx.token);
+                AssertEqual(true, result.IsOk, $"Runtime.getProperties failed for {cfo_args.ToString ()}, with Result: {result}");
+                id = result.Value["result"] ? ["objectId"]?.Value<string>();
+            }
+
+            var get_prop_req = JObject.FromObject(new
+            {
+                objectId = id
+            });
+
+            var frame_props = await ctx.cli.SendCommand("Runtime.getProperties", get_prop_req, ctx.token);
+            if (!frame_props.IsOk)
+                Assert.True(false, $"Runtime.getProperties failed for {get_prop_req.ToString ()}, with Result: {frame_props}");
+
+            var locals = frame_props.Value["result"];
+            // FIXME: Should be done when generating the list in library_mono.js, but not sure yet
+            //        whether to remove it, and how to do it correctly.
+            if (locals is JArray)
+            {
+                foreach (var p in locals)
+                {
+                    if (p["name"]?.Value<string>() == "length" && p["enumerable"]?.Value<bool>() != true)
+                    {
+                        p.Remove();
+                        break;
+                    }
+                }
+            }
+
+            return locals;
+        }
+
+        internal async Task<JToken> EvaluateOnCallFrame(string id, string expression)
+        {
+            var evaluate_req = JObject.FromObject(new
+            {
+                callFrameId = id,
+                    expression = expression
+            });
+
+            var frame_evaluate = await ctx.cli.SendCommand("Debugger.evaluateOnCallFrame", evaluate_req, ctx.token);
+            if (!frame_evaluate.IsOk)
+                Assert.True(false, $"Debugger.evaluateOnCallFrame failed for {evaluate_req.ToString ()}, with Result: {frame_evaluate}");
+
+            var evaluate_result = frame_evaluate.Value["result"];
+            return evaluate_result;
+        }
+
+        internal async Task<Result> SetBreakpoint(string url_key, int line, int column, bool expect_ok = true, bool use_regex = false)
+        {
+            var bp1_req = !use_regex ?
+                JObject.FromObject(new { lineNumber = line, columnNumber = column, url = dicFileToUrl[url_key], }) :
+                JObject.FromObject(new { lineNumber = line, columnNumber = column, urlRegex = url_key, });
+
+            var bp1_res = await ctx.cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, ctx.token);
+            Assert.True(expect_ok ? bp1_res.IsOk : bp1_res.IsErr);
+
+            return bp1_res;
+        }
+
+        internal async Task<Result> SetBreakpointInMethod(string assembly, string type, string method, int lineOffset = 0, int col = 0)
+        {
+            var req = JObject.FromObject(new { assemblyName = assembly, typeName = type, methodName = method, lineOffset = lineOffset });
+
+            // Protocol extension
+            var res = await ctx.cli.SendCommand("DotnetDebugger.getMethodLocation", req, ctx.token);
+            Assert.True(res.IsOk);
+
+            var m_url = res.Value["result"]["url"].Value<string>();
+            var m_line = res.Value["result"]["line"].Value<int>();
+
+            var bp1_req = JObject.FromObject(new
+            {
+                lineNumber = m_line + lineOffset,
+                    columnNumber = col,
+                    url = m_url
+            });
+
+            res = await ctx.cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, ctx.token);
+            Assert.True(res.IsOk);
+
+            return res;
+        }
+
+        internal void AssertEqual(object expected, object actual, string label) => Assert.True(expected?.Equals(actual),
+            $"[{label}]\n" +
+            $"Expected: {expected?.ToString()}\n" +
+            $"Actual:   {actual?.ToString()}\n");
+
+        internal void AssertStartsWith(string expected, string actual, string label) => Assert.True(actual?.StartsWith(expected), $"[{label}] Does not start with the expected string\nExpected: {expected}\nActual:   {actual}");
+
+        internal static Func<int, int, string, string, object> TSimpleClass = (X, Y, Id, Color) => new
+        {
+            X = TNumber(X),
+            Y = TNumber(Y),
+            Id = TString(Id),
+            Color = TEnum("DebuggerTests.RGB", Color),
+            PointWithCustomGetter = TGetter("PointWithCustomGetter")
+        };
+
+        internal static Func<int, int, string, string, object> TPoint = (X, Y, Id, Color) => new
+        {
+            X = TNumber(X),
+            Y = TNumber(Y),
+            Id = TString(Id),
+            Color = TEnum("DebuggerTests.RGB", Color),
+        };
+
+        //FIXME: um maybe we don't need to convert jobject right here!
+        internal static JObject TString(string value) =>
+            value == null ?
+            TObject("string", is_null : true) :
+            JObject.FromObject(new { type = "string", value = @value, description = @value });
+
+        internal static JObject TNumber(int value) =>
+            JObject.FromObject(new { type = "number", value = @value.ToString(), description = value.ToString() });
+
+        internal static JObject TValueType(string className, string description = null, object members = null) =>
+            JObject.FromObject(new { type = "object", isValueType = true, className = className, description = description ?? className });
+
+        internal static JObject TEnum(string className, string descr, object members = null) =>
+            JObject.FromObject(new { type = "object", isEnum = true, className = className, description = descr });
+
+        internal static JObject TObject(string className, string description = null, bool is_null = false) =>
+            is_null ?
+            JObject.FromObject(new { type = "object", className = className, description = description ?? className, subtype = is_null ? "null" : null }) :
+            JObject.FromObject(new { type = "object", className = className, description = description ?? className });
+
+        internal static JObject TArray(string className, int length = 0) => JObject.FromObject(new { type = "object", className = className, description = $"{className}({length})", subtype = "array" });
+
+        internal static JObject TBool(bool value) => JObject.FromObject(new { type = "boolean", value = @value, description = @value ? "true" : "false" });
+
+        internal static JObject TSymbol(string value) => JObject.FromObject(new { type = "symbol", value = @value, description = @value });
+
+        /*
+               For target names with generated method names like
+                       `void <ActionTSignatureTest>b__11_0 (Math.GenericStruct<int[]>)`
+
+               .. pass target "as `target: "void <ActionTSignatureTest>|(Math.GenericStruct<int[]>)"`
+        */
+        internal static JObject TDelegate(string className, string target) => JObject.FromObject(new
+        {
+            __custom_type = "delegate",
+                className = className,
+                target = target
+        });
+
+        internal static JObject TPointer(string type_name, bool is_null = false) => JObject.FromObject(new { __custom_type = "pointer", type_name = type_name, is_null = is_null, is_void = type_name.StartsWith("void*") });
+
+        internal static JObject TIgnore() => JObject.FromObject(new { __custom_type = "ignore_me" });
+
+        internal static JObject TGetter(string type) => JObject.FromObject(new { __custom_type = "getter", type_name = type });
+    }
+
+    class DebugTestContext
+    {
+        public InspectorClient cli;
+        public Inspector insp;
+        public CancellationToken token;
+        public Dictionary<string, string> scripts;
+
+        public bool UseCallFunctionOnBeforeGetProperties;
+
+        public DebugTestContext(InspectorClient cli, Inspector insp, CancellationToken token, Dictionary<string, string> scripts)
+        {
+            this.cli = cli;
+            this.insp = insp;
+            this.token = token;
+            this.scripts = scripts;
+        }
+    }
+
+    enum StepKind
+    {
+        Into,
+        Over,
+        Out
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs
new file mode 100644 (file)
index 0000000..03964dd
--- /dev/null
@@ -0,0 +1,1483 @@
+// 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 Newtonsoft.Json.Linq;
+using Microsoft.WebAssembly.Diagnostics;
+using Xunit;
+
+[assembly : CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
+
+namespace DebuggerTests
+{
+
+    public class SourceList : DebuggerTestBase
+    {
+
+        [Fact]
+        public async Task CheckThatAllSourcesAreSent()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            //all sources are sent before runtime ready is sent, nothing to check
+            await insp.Ready();
+            Assert.Contains("dotnet://debugger-test.dll/debugger-test.cs", scripts.Values);
+            Assert.Contains("dotnet://debugger-test.dll/debugger-test2.cs", scripts.Values);
+            Assert.Contains("dotnet://debugger-test.dll/dependency.cs", scripts.Values);
+        }
+
+        [Fact]
+        public async Task CreateGoodBreakpoint()
+        {
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bp1_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8);
+
+                Assert.EndsWith("debugger-test.cs", bp1_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+                Assert.NotNull(loc["scriptId"]);
+                Assert.Equal("dotnet://debugger-test.dll/debugger-test.cs", scripts[loc["scriptId"]?.Value<string>()]);
+                Assert.Equal(10, loc["lineNumber"]);
+                Assert.Equal(8, loc["columnNumber"]);
+            });
+        }
+
+        [Fact]
+        public async Task CreateJSBreakpoint()
+        {
+            // Test that js breakpoints get set correctly
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                // 13 24
+                // 13 31
+                var bp1_res = await SetBreakpoint("/debugger-driver.html", 13, 24);
+
+                Assert.EndsWith("debugger-driver.html", bp1_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+                Assert.NotNull(loc["scriptId"]);
+                Assert.Equal(13, loc["lineNumber"]);
+                Assert.Equal(24, loc["columnNumber"]);
+
+                var bp2_res = await SetBreakpoint("/debugger-driver.html", 13, 31);
+
+                Assert.EndsWith("debugger-driver.html", bp2_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp2_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc2 = bp2_res.Value["locations"]?.Value<JArray>() [0];
+
+                Assert.NotNull(loc2["scriptId"]);
+                Assert.Equal(13, loc2["lineNumber"]);
+                Assert.Equal(31, loc2["columnNumber"]);
+            });
+        }
+
+        [Fact]
+        public async Task CreateJS0Breakpoint()
+        {
+            // Test that js column 0 does as expected
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                // 13 24
+                // 13 31
+                var bp1_res = await SetBreakpoint("/debugger-driver.html", 13, 0);
+
+                Assert.EndsWith("debugger-driver.html", bp1_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+                Assert.NotNull(loc["scriptId"]);
+                Assert.Equal(13, loc["lineNumber"]);
+                Assert.Equal(24, loc["columnNumber"]);
+
+                var bp2_res = await SetBreakpoint("/debugger-driver.html", 13, 31);
+
+                Assert.EndsWith("debugger-driver.html", bp2_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp2_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc2 = bp2_res.Value["locations"]?.Value<JArray>() [0];
+
+                Assert.NotNull(loc2["scriptId"]);
+                Assert.Equal(13, loc2["lineNumber"]);
+                Assert.Equal(31, loc2["columnNumber"]);
+            });
+        }
+
+        [Theory]
+        [InlineData(0)]
+        [InlineData(50)]
+        public async Task CheckMultipleBreakpointsOnSameLine(int col)
+        {
+            var insp = new Inspector();
+
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bp1_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-array-test.cs", 219, col);
+                Assert.EndsWith("debugger-array-test.cs", bp1_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp1_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc = bp1_res.Value["locations"]?.Value<JArray>() [0];
+
+                CheckLocation("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 50, scripts, loc);
+
+                var bp2_res = await SetBreakpoint("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 55);
+                Assert.EndsWith("debugger-array-test.cs", bp2_res.Value["breakpointId"].ToString());
+                Assert.Equal(1, bp2_res.Value["locations"]?.Value<JArray>()?.Count);
+
+                var loc2 = bp2_res.Value["locations"]?.Value<JArray>() [0];
+
+                CheckLocation("dotnet://debugger-test.dll/debugger-array-test.cs", 219, 55, scripts, loc2);
+            });
+        }
+
+        [Fact]
+        public async Task CreateBadBreakpoint()
+        {
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                var bp1_req = JObject.FromObject(new
+                {
+                    lineNumber = 8,
+                        columnNumber = 2,
+                        url = "dotnet://debugger-test.dll/this-file-doesnt-exist.cs",
+                });
+
+                var bp1_res = await cli.SendCommand("Debugger.setBreakpointByUrl", bp1_req, token);
+
+                Assert.True(bp1_res.IsOk);
+                Assert.Empty(bp1_res.Value["locations"].Values<object>());
+                //Assert.Equal ((int)MonoErrorCodes.BpNotFound, bp1_res.Error ["code"]?.Value<int> ());
+            });
+        }
+
+        [Fact]
+        public async Task CreateGoodBreakpointAndHit()
+        {
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8);
+
+                var eval_req = JObject.FromObject(new
+                {
+                    expression = "window.setTimeout(function() { invoke_add(); }, 1);",
+                });
+
+                await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_add(); }, 1);",
+                    "dotnet://debugger-test.dll/debugger-test.cs", 10, 8,
+                    "IntAdd",
+                    wait_for_event_fn: (pause_location) =>
+                    {
+                        Assert.Equal("other", pause_location["reason"]?.Value<string>());
+                        Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+                        var top_frame = pause_location["callFrames"][0];
+                        Assert.Equal("IntAdd", top_frame["functionName"].Value<string>());
+                        Assert.Contains("debugger-test.cs", top_frame["url"].Value<string>());
+
+                        CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, top_frame["functionLocation"]);
+
+                        //now check the scope
+                        var scope = top_frame["scopeChain"][0];
+                        Assert.Equal("local", scope["type"]);
+                        Assert.Equal("IntAdd", scope["name"]);
+
+                        Assert.Equal("object", scope["object"]["type"]);
+                        Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+                        CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, scope["startLocation"]);
+                        CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 14, 4, scripts, scope["endLocation"]);
+                        return Task.CompletedTask;
+                    }
+                );
+
+            });
+        }
+
+        [Fact]
+        public async Task ExceptionThrownInJS()
+        {
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                var eval_req = JObject.FromObject(new
+                {
+                    expression = "invoke_bad_js_test();"
+                });
+
+                var eval_res = await cli.SendCommand("Runtime.evaluate", eval_req, token);
+                Assert.True(eval_res.IsErr);
+                Assert.Equal("Uncaught", eval_res.Error["exceptionDetails"] ? ["text"]?.Value<string>());
+            });
+        }
+
+        [Fact]
+        public async Task ExceptionThrownInJSOutOfBand()
+        {
+            var insp = new Inspector();
+
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                await SetBreakpoint("/debugger-driver.html", 27, 2);
+
+                var eval_req = JObject.FromObject(new
+                {
+                    expression = "window.setTimeout(function() { invoke_bad_js_test(); }, 1);",
+                });
+
+                var eval_res = await cli.SendCommand("Runtime.evaluate", eval_req, token);
+                // Response here will be the id for the timer from JS!
+                Assert.True(eval_res.IsOk);
+
+                var ex = await Assert.ThrowsAsync<ArgumentException>(async() => await insp.WaitFor("Runtime.exceptionThrown"));
+                var ex_json = JObject.Parse(ex.Message);
+                Assert.Equal(dicFileToUrl["/debugger-driver.html"], ex_json["exceptionDetails"] ? ["url"]?.Value<string>());
+            });
+
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsAtBreakpointSite(bool use_cfo) =>
+            await CheckInspectLocalsAtBreakpointSite(
+                "dotnet://debugger-test.dll/debugger-test.cs", 10, 8, "IntAdd",
+                "window.setTimeout(function() { invoke_add(); }, 1);",
+                use_cfo : use_cfo,
+                test_fn: (locals) =>
+                {
+                    CheckNumber(locals, "a", 10);
+                    CheckNumber(locals, "b", 20);
+                    CheckNumber(locals, "c", 30);
+                    CheckNumber(locals, "d", 0);
+                    CheckNumber(locals, "e", 0);
+                }
+            );
+
+        [Fact]
+        public async Task InspectPrimitiveTypeLocalsAtBreakpointSite() =>
+            await CheckInspectLocalsAtBreakpointSite(
+                "dotnet://debugger-test.dll/debugger-test.cs", 154, 8, "PrimitiveTypesTest",
+                "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:PrimitiveTypesTest'); }, 1);",
+                test_fn: (locals) =>
+                {
+                    CheckSymbol(locals, "c0", "8364 '€'");
+                    CheckSymbol(locals, "c1", "65 'A'");
+                }
+            );
+
+        [Fact]
+        public async Task InspectLocalsTypesAtBreakpointSite() =>
+            await CheckInspectLocalsAtBreakpointSite(
+                "dotnet://debugger-test.dll/debugger-test2.cs", 48, 8, "Types",
+                "window.setTimeout(function() { invoke_static_method (\"[debugger-test] Fancy:Types\")(); }, 1);",
+                use_cfo : false,
+                test_fn: (locals) =>
+                {
+                    CheckNumber(locals, "dPI", Math.PI);
+                    CheckNumber(locals, "fPI", (float) Math.PI);
+                    CheckNumber(locals, "iMax", int.MaxValue);
+                    CheckNumber(locals, "iMin", int.MinValue);
+                    CheckNumber(locals, "uiMax", uint.MaxValue);
+                    CheckNumber(locals, "uiMin", uint.MinValue);
+
+                    CheckNumber(locals, "l", uint.MaxValue * (long) 2);
+                    //CheckNumber (locals, "lMax", long.MaxValue); // cannot be represented as double
+                    //CheckNumber (locals, "lMin", long.MinValue); // cannot be represented as double
+
+                    CheckNumber(locals, "sbMax", sbyte.MaxValue);
+                    CheckNumber(locals, "sbMin", sbyte.MinValue);
+                    CheckNumber(locals, "bMax", byte.MaxValue);
+                    CheckNumber(locals, "bMin", byte.MinValue);
+
+                    CheckNumber(locals, "sMax", short.MaxValue);
+                    CheckNumber(locals, "sMin", short.MinValue);
+                    CheckNumber(locals, "usMin", ushort.MinValue);
+                    CheckNumber(locals, "usMax", ushort.MaxValue);
+                }
+            );
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsWithGenericTypesAtBreakpointSite(bool use_cfo) =>
+            await CheckInspectLocalsAtBreakpointSite(
+                "dotnet://debugger-test.dll/debugger-test.cs", 74, 8, "GenericTypesTest",
+                "window.setTimeout(function() { invoke_generic_types_test (); }, 1);",
+                use_cfo : use_cfo,
+                test_fn: (locals) =>
+                {
+                    CheckObject(locals, "list", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>");
+                    CheckObject(locals, "list_null", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>", is_null : true);
+
+                    CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]");
+                    CheckObject(locals, "list_arr_null", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", is_null : true);
+
+                    // Unused locals
+                    CheckObject(locals, "list_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>");
+                    CheckObject(locals, "list_null_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>", is_null : true);
+
+                    CheckObject(locals, "list_arr_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]");
+                    CheckObject(locals, "list_arr_null_unused", "System.Collections.Generic.Dictionary<Math[], Math.IsMathNull>[]", is_null : true);
+                }
+            );
+
+        object TGenericStruct(string typearg, string stringField) => new
+        {
+            List = TObject($"System.Collections.Generic.List<{typearg}>"),
+            StringField = TString(stringField)
+        };
+
+        [Fact]
+        public async Task RuntimeGetPropertiesWithInvalidScopeIdTest()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 49, 8);
+
+                await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_delegates_test (); }, 1);",
+                    "dotnet://debugger-test.dll/debugger-test.cs", 49, 8,
+                    "DelegatesTest",
+                    wait_for_event_fn : async(pause_location) =>
+                    {
+                        //make sure we're on the right bp
+                        Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+                        var top_frame = pause_location["callFrames"][0];
+
+                        var scope = top_frame["scopeChain"][0];
+                        Assert.Equal("dotnet:scope:0", scope["object"]["objectId"]);
+
+                        // Try to get an invalid scope!
+                        var get_prop_req = JObject.FromObject(new
+                        {
+                            objectId = "dotnet:scope:23490871",
+                        });
+
+                        var frame_props = await cli.SendCommand("Runtime.getProperties", get_prop_req, token);
+                        Assert.True(frame_props.IsErr);
+                    }
+                );
+            });
+        }
+
+        [Fact]
+        public async Task TrivalStepping()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 10, 8);
+
+                await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_add(); }, 1);",
+                    "dotnet://debugger-test.dll/debugger-test.cs", 10, 8,
+                    "IntAdd",
+                    wait_for_event_fn: (pause_location) =>
+                    {
+                        //make sure we're on the right bp
+                        Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+                        var top_frame = pause_location["callFrames"][0];
+                        CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, top_frame["functionLocation"]);
+                        return Task.CompletedTask;
+                    }
+                );
+
+                await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 11, 8, "IntAdd",
+                    wait_for_event_fn: (pause_location) =>
+                    {
+                        var top_frame = pause_location["callFrames"][0];
+                        CheckLocation("dotnet://debugger-test.dll/debugger-test.cs", 8, 4, scripts, top_frame["functionLocation"]);
+                        return Task.CompletedTask;
+                    }
+                );
+            });
+        }
+
+        [Fact]
+        public async Task InspectLocalsDuringStepping()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+                await SetBreakpoint(debugger_test_loc, 10, 8);
+
+                await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_add(); }, 1);",
+                    debugger_test_loc, 10, 8, "IntAdd",
+                    locals_fn: (locals) =>
+                    {
+                        CheckNumber(locals, "a", 10);
+                        CheckNumber(locals, "b", 20);
+                        CheckNumber(locals, "c", 30);
+                        CheckNumber(locals, "d", 0);
+                        CheckNumber(locals, "e", 0);
+                    }
+                );
+
+                await StepAndCheck(StepKind.Over, debugger_test_loc, 11, 8, "IntAdd",
+                    locals_fn: (locals) =>
+                    {
+                        CheckNumber(locals, "a", 10);
+                        CheckNumber(locals, "b", 20);
+                        CheckNumber(locals, "c", 30);
+                        CheckNumber(locals, "d", 50);
+                        CheckNumber(locals, "e", 0);
+                    }
+                );
+
+                //step and get locals
+                await StepAndCheck(StepKind.Over, debugger_test_loc, 12, 8, "IntAdd",
+                    locals_fn: (locals) =>
+                    {
+                        CheckNumber(locals, "a", 10);
+                        CheckNumber(locals, "b", 20);
+                        CheckNumber(locals, "c", 30);
+                        CheckNumber(locals, "d", 50);
+                        CheckNumber(locals, "e", 60);
+                    }
+                );
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsInPreviousFramesDuringSteppingIn2(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+                var dep_cs_loc = "dotnet://debugger-test.dll/dependency.cs";
+                await SetBreakpoint(dep_cs_loc, 33, 8);
+
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+
+                // Will stop in Complex.DoEvenMoreStuff
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_use_complex (); }, 1);",
+                    dep_cs_loc, 33, 8, "DoEvenMoreStuff",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Single(locals);
+                        CheckObject(locals, "this", "Simple.Complex");
+                    }
+                );
+
+                var props = await GetObjectOnFrame(pause_location["callFrames"][0], "this");
+                Assert.Equal(3, props.Count());
+                CheckNumber(props, "A", 10);
+                CheckString(props, "B", "xx");
+                CheckObject(props, "c", "object");
+
+                // Check UseComplex frame
+                var locals_m1 = await GetLocalsForFrame(pause_location["callFrames"][3], debugger_test_loc, 23, 8, "UseComplex");
+                Assert.Equal(7, locals_m1.Count());
+
+                CheckNumber(locals_m1, "a", 10);
+                CheckNumber(locals_m1, "b", 20);
+                CheckObject(locals_m1, "complex", "Simple.Complex");
+                CheckNumber(locals_m1, "c", 30);
+                CheckNumber(locals_m1, "d", 50);
+                CheckNumber(locals_m1, "e", 60);
+                CheckNumber(locals_m1, "f", 0);
+
+                props = await GetObjectOnFrame(pause_location["callFrames"][3], "complex");
+                Assert.Equal(3, props.Count());
+                CheckNumber(props, "A", 10);
+                CheckString(props, "B", "xx");
+                CheckObject(props, "c", "object");
+
+                pause_location = await StepAndCheck(StepKind.Over, dep_cs_loc, 23, 8, "DoStuff", times : 2);
+                // Check UseComplex frame again
+                locals_m1 = await GetLocalsForFrame(pause_location["callFrames"][1], debugger_test_loc, 23, 8, "UseComplex");
+                Assert.Equal(7, locals_m1.Count());
+
+                CheckNumber(locals_m1, "a", 10);
+                CheckNumber(locals_m1, "b", 20);
+                CheckObject(locals_m1, "complex", "Simple.Complex");
+                CheckNumber(locals_m1, "c", 30);
+                CheckNumber(locals_m1, "d", 50);
+                CheckNumber(locals_m1, "e", 60);
+                CheckNumber(locals_m1, "f", 0);
+
+                props = await GetObjectOnFrame(pause_location["callFrames"][1], "complex");
+                Assert.Equal(3, props.Count());
+                CheckNumber(props, "A", 10);
+                CheckString(props, "B", "xx");
+                CheckObject(props, "c", "object");
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsInPreviousFramesDuringSteppingIn(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+                await SetBreakpoint(debugger_test_loc, 111, 12);
+
+                // Will stop in InnerMethod
+                var wait_res = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_outer_method(); }, 1);",
+                    debugger_test_loc, 111, 12, "InnerMethod",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(4, locals.Count());
+                        CheckNumber(locals, "i", 5);
+                        CheckNumber(locals, "j", 24);
+                        CheckString(locals, "foo_str", "foo");
+                        CheckObject(locals, "this", "Math.NestedInMath");
+                    }
+                );
+
+                var this_props = await GetObjectOnFrame(wait_res["callFrames"][0], "this");
+                Assert.Equal(2, this_props.Count());
+                CheckObject(this_props, "m", "Math");
+                CheckValueType(this_props, "SimpleStructProperty", "Math.SimpleStruct");
+
+                var ss_props = await GetObjectOnLocals(this_props, "SimpleStructProperty");
+                Assert.Equal(2, ss_props.Count());
+                CheckValueType(ss_props, "dt", "System.DateTime");
+                CheckValueType(ss_props, "gs", "Math.GenericStruct<System.DateTime>");
+
+                await CheckDateTime(ss_props, "dt", new DateTime(2020, 1, 2, 3, 4, 5));
+
+                // Check OuterMethod frame
+                var locals_m1 = await GetLocalsForFrame(wait_res["callFrames"][1], debugger_test_loc, 87, 8, "OuterMethod");
+                Assert.Equal(5, locals_m1.Count());
+                // FIXME: Failing test CheckNumber (locals_m1, "i", 5);
+                // FIXME: Failing test CheckString (locals_m1, "text", "Hello");
+                CheckNumber(locals_m1, "new_i", 0);
+                CheckNumber(locals_m1, "k", 0);
+                CheckObject(locals_m1, "nim", "Math.NestedInMath");
+
+                // step back into OuterMethod
+                await StepAndCheck(StepKind.Over, debugger_test_loc, 91, 8, "OuterMethod", times : 9,
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(5, locals.Count());
+
+                        // FIXME: Failing test CheckNumber (locals_m1, "i", 5);
+                        CheckString(locals, "text", "Hello");
+                        // FIXME: Failing test CheckNumber (locals, "new_i", 24);
+                        CheckNumber(locals, "k", 19);
+                        CheckObject(locals, "nim", "Math.NestedInMath");
+                    }
+                );
+
+                //await StepAndCheck (StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 81, 2, "OuterMethod", times: 2);
+
+                // step into InnerMethod2
+                await StepAndCheck(StepKind.Into, "dotnet://debugger-test.dll/debugger-test.cs", 96, 4, "InnerMethod2",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(3, locals.Count());
+
+                        CheckString(locals, "s", "test string");
+                        //out var: CheckNumber (locals, "k", 0);
+                        CheckNumber(locals, "i", 24);
+                    }
+                );
+
+                await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 100, 4, "InnerMethod2", times : 4,
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(3, locals.Count());
+
+                        CheckString(locals, "s", "test string");
+                        // FIXME: Failing test CheckNumber (locals, "k", 34);
+                        CheckNumber(locals, "i", 24);
+                    }
+                );
+
+                await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 92, 8, "OuterMethod", times : 2,
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(5, locals.Count());
+
+                        CheckString(locals, "text", "Hello");
+                        // FIXME: failing test CheckNumber (locals, "i", 5);
+                        CheckNumber(locals, "new_i", 22);
+                        CheckNumber(locals, "k", 34);
+                        CheckObject(locals, "nim", "Math.NestedInMath");
+                    }
+                );
+            });
+        }
+
+        [Fact]
+        public async Task InspectLocalsDuringSteppingIn()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 86, 8);
+
+                await EvaluateAndCheck("window.setTimeout(function() { invoke_outer_method(); }, 1);",
+                    "dotnet://debugger-test.dll/debugger-test.cs", 86, 8, "OuterMethod",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(5, locals.Count());
+
+                        CheckObject(locals, "nim", "Math.NestedInMath");
+                        CheckNumber(locals, "i", 5);
+                        CheckNumber(locals, "k", 0);
+                        CheckNumber(locals, "new_i", 0);
+                        CheckString(locals, "text", null);
+                    }
+                );
+
+                await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 87, 8, "OuterMethod",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(5, locals.Count());
+
+                        CheckObject(locals, "nim", "Math.NestedInMath");
+                        // FIXME: Failing test CheckNumber (locals, "i", 5);
+                        CheckNumber(locals, "k", 0);
+                        CheckNumber(locals, "new_i", 0);
+                        CheckString(locals, "text", "Hello");
+                    }
+                );
+
+                // Step into InnerMethod
+                await StepAndCheck(StepKind.Into, "dotnet://debugger-test.dll/debugger-test.cs", 105, 8, "InnerMethod");
+                await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 109, 12, "InnerMethod", times : 5,
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(4, locals.Count());
+
+                        CheckNumber(locals, "i", 5);
+                        CheckNumber(locals, "j", 15);
+                        CheckString(locals, "foo_str", "foo");
+                        CheckObject(locals, "this", "Math.NestedInMath");
+                    }
+                );
+
+                // Step back to OuterMethod
+                await StepAndCheck(StepKind.Over, "dotnet://debugger-test.dll/debugger-test.cs", 88, 8, "OuterMethod", times : 6,
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(5, locals.Count());
+
+                        CheckObject(locals, "nim", "Math.NestedInMath");
+                        // FIXME: Failing test CheckNumber (locals, "i", 5);
+                        CheckNumber(locals, "k", 0);
+                        CheckNumber(locals, "new_i", 24);
+                        CheckString(locals, "text", "Hello");
+                    }
+                );
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsInAsyncMethods(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, 120, 12);
+                await SetBreakpoint(debugger_test_loc, 135, 12);
+
+                // Will stop in Asyncmethod0
+                var wait_res = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_async_method_with_await(); }, 1);",
+                    debugger_test_loc, 120, 12, "MoveNext", //FIXME:
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(4, locals.Count());
+                        CheckString(locals, "s", "string from js");
+                        CheckNumber(locals, "i", 42);
+                        CheckString(locals, "local0", "value0");
+                        CheckObject(locals, "this", "Math.NestedInMath");
+                    }
+                );
+                Console.WriteLine(wait_res);
+
+#if false // Disabled for now, as we don't have proper async traces
+                var locals = await GetProperties(wait_res["callFrames"][2]["callFrameId"].Value<string>());
+                Assert.Equal(4, locals.Count());
+                CheckString(locals, "ls", "string from jstest");
+                CheckNumber(locals, "li", 52);
+#endif
+
+                // TODO: previous frames have async machinery details, so no point checking that right now
+
+                var pause_loc = await SendCommandAndCheck(null, "Debugger.resume", debugger_test_loc, 135, 12, /*FIXME: "AsyncMethodNoReturn"*/ "MoveNext",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(4, locals.Count());
+                        CheckString(locals, "str", "AsyncMethodNoReturn's local");
+                        CheckObject(locals, "this", "Math.NestedInMath");
+                        //FIXME: check fields
+                        CheckValueType(locals, "ss", "Math.SimpleStruct");
+                        CheckArray(locals, "ss_arr", "Math.SimpleStruct[]");
+                        // TODO: struct fields
+                    }
+                );
+
+                var this_props = await GetObjectOnFrame(pause_loc["callFrames"][0], "this");
+                Assert.Equal(2, this_props.Count());
+                CheckObject(this_props, "m", "Math");
+                CheckValueType(this_props, "SimpleStructProperty", "Math.SimpleStruct");
+
+                // TODO: Check `this` properties
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsWithStructs(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, 22, 8);
+
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_method_with_structs(); }, 1);",
+                    debugger_test_loc, 22, 8, "MethodWithLocalStructs",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(3, locals.Count());
+
+                        CheckValueType(locals, "ss_local", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                        CheckValueType(locals, "gs_local", "DebuggerTests.ValueTypesTest.GenericStruct<DebuggerTests.ValueTypesTest>");
+                        CheckObject(locals, "vt_local", "DebuggerTests.ValueTypesTest");
+                    }
+                );
+
+                var dt = new DateTime(2021, 2, 3, 4, 6, 7);
+                // Check ss_local's properties
+                var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
+                await CheckProps(ss_local_props, new
+                {
+                    str_member = TString("set in MethodWithLocalStructs#SimpleStruct#str_member"),
+                        dt = TValueType("System.DateTime", dt.ToString()),
+                        gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                        Kind = TEnum("System.DateTimeKind", "Utc")
+                }, "ss_local");
+
+                {
+                    // Check ss_local.dt
+                    await CheckDateTime(ss_local_props, "dt", dt);
+
+                    // Check ss_local.gs
+                    var gs_props = await GetObjectOnLocals(ss_local_props, "gs");
+                    CheckString(gs_props, "StringField", "set in MethodWithLocalStructs#SimpleStruct#gs#StringField");
+                    CheckObject(gs_props, "List", "System.Collections.Generic.List<System.DateTime>");
+                }
+
+                // Check gs_local's properties
+                var gs_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "gs_local");
+                await CheckProps(gs_local_props, new
+                {
+                    StringField = TString("gs_local#GenericStruct<ValueTypesTest>#StringField"),
+                        List = TObject("System.Collections.Generic.List<DebuggerTests.ValueTypesTest>", is_null : true),
+                        Options = TEnum("DebuggerTests.Options", "None")
+                }, "gs_local");
+
+                // Check vt_local's properties
+                var vt_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "vt_local");
+                Assert.Equal(5, vt_local_props.Count());
+
+                CheckString(vt_local_props, "StringField", "string#0");
+                CheckValueType(vt_local_props, "SimpleStructField", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                CheckValueType(vt_local_props, "SimpleStructProperty", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                await CheckDateTime(vt_local_props, "DT", new DateTime(2020, 1, 2, 3, 4, 5));
+                CheckEnum(vt_local_props, "RGB", "DebuggerTests.RGB", "Blue");
+
+                {
+                    // SimpleStructProperty
+                    dt = new DateTime(2022, 3, 4, 5, 7, 8);
+                    var ssp_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructProperty",
+                        new
+                        {
+                            str_member = TString("SimpleStructProperty#string#0#SimpleStruct#str_member"),
+                                dt = TValueType("System.DateTime", dt.ToString()),
+                                gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                                Kind = TEnum("System.DateTimeKind", "Utc")
+                        },
+                        label: "vt_local_props.SimpleStructProperty");
+
+                    await CheckDateTime(ssp_props, "dt", dt);
+
+                    // SimpleStructField
+                    dt = new DateTime(2025, 6, 7, 8, 10, 11);
+                    var ssf_props = await CompareObjectPropertiesFor(vt_local_props, "SimpleStructField",
+                        new
+                        {
+                            str_member = TString("SimpleStructField#string#0#SimpleStruct#str_member"),
+                                dt = TValueType("System.DateTime", dt.ToString()),
+                                gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                                Kind = TEnum("System.DateTimeKind", "Local")
+                        },
+                        label: "vt_local_props.SimpleStructField");
+
+                    await CheckDateTime(ssf_props, "dt", dt);
+                }
+
+                // FIXME: check ss_local.gs.List's members
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectValueTypeMethodArgs(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, 34, 12);
+
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:TestStructsAsMethodArgs'); }, 1);",
+                    debugger_test_loc, 34, 12, "MethodWithStructArgs",
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(3, locals.Count());
+
+                        CheckString(locals, "label", "TestStructsAsMethodArgs#label");
+                        CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                        CheckNumber(locals, "x", 3);
+                    }
+                );
+
+                var dt = new DateTime(2025, 6, 7, 8, 10, 11);
+                var ss_local_as_ss_arg = new
+                {
+                    str_member = TString("ss_local#SimpleStruct#string#0#SimpleStruct#str_member"),
+                    dt = TValueType("System.DateTime", dt.ToString()),
+                    gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                    Kind = TEnum("System.DateTimeKind", "Local")
+                };
+                var ss_local_gs = new
+                {
+                    StringField = TString("ss_local#SimpleStruct#string#0#SimpleStruct#gs#StringField"),
+                    List = TObject("System.Collections.Generic.List<System.DateTime>"),
+                    Options = TEnum("DebuggerTests.Options", "Option1")
+                };
+
+                // Check ss_arg's properties
+                var ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg");
+                await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_arg");
+
+                {
+                    // Check ss_local.dt
+                    await CheckDateTime(ss_arg_props, "dt", dt);
+
+                    // Check ss_local.gs
+                    await CompareObjectPropertiesFor(ss_arg_props, "gs", ss_local_gs);
+                }
+
+                pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 38, 8, "MethodWithStructArgs", times : 4,
+                    locals_fn: (locals) =>
+                    {
+                        Assert.Equal(3, locals.Count());
+
+                        CheckString(locals, "label", "TestStructsAsMethodArgs#label");
+                        CheckValueType(locals, "ss_arg", "DebuggerTests.ValueTypesTest.SimpleStruct");
+                        CheckNumber(locals, "x", 3);
+
+                    }
+                );
+
+                var ss_arg_updated = new
+                {
+                    str_member = TString("ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member"),
+                    dt = TValueType("System.DateTime", dt.ToString()),
+                    gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                    Kind = TEnum("System.DateTimeKind", "Utc")
+                };
+
+                ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_arg");
+                await CheckProps(ss_arg_props, ss_arg_updated, "ss_ar");
+
+                {
+                    // Check ss_local.gs
+                    await CompareObjectPropertiesFor(ss_arg_props, "gs", new
+                    {
+                        StringField = TString("ValueTypesTest#MethodWithStructArgs#updated#gs#StringField#3"),
+                            List = TObject("System.Collections.Generic.List<System.DateTime>"),
+                            Options = TEnum("DebuggerTests.Options", "Option1")
+                    });
+
+                    await CheckDateTime(ss_arg_props, "dt", dt);
+                }
+
+                // Check locals on previous frame, same as earlier in this test
+                ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][1], "ss_local");
+                await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_local");
+
+                {
+                    // Check ss_local.dt
+                    await CheckDateTime(ss_arg_props, "dt", dt);
+
+                    // Check ss_local.gs
+                    var gs_props = await GetObjectOnLocals(ss_arg_props, "gs");
+                    CheckString(gs_props, "StringField", "ss_local#SimpleStruct#string#0#SimpleStruct#gs#StringField");
+                    CheckObject(gs_props, "List", "System.Collections.Generic.List<System.DateTime>");
+                }
+
+                // ----------- Step back to the caller ---------
+
+                pause_location = await StepAndCheck(StepKind.Over, debugger_test_loc, 28, 12, "TestStructsAsMethodArgs",
+                    times : 2, locals_fn: (l) => { /* non-null to make sure that locals get fetched */ });
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(locals, new
+                    {
+                        ss_local = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct"),
+                            ss_ret = TValueType("DebuggerTests.ValueTypesTest.SimpleStruct")
+                    },
+                    "locals#0");
+
+                ss_arg_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
+                await CheckProps(ss_arg_props, ss_local_as_ss_arg, "ss_local");
+
+                {
+                    // Check ss_local.gs
+                    await CompareObjectPropertiesFor(ss_arg_props, "gs", ss_local_gs, label: "ss_local_gs");
+                }
+
+                // FIXME: check ss_local.gs.List's members
+            });
+        }
+
+        [Fact]
+        public async Task CheckUpdatedValueTypeFieldsOnResume()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                var lines = new [] { 202, 205 };
+                await SetBreakpoint(debugger_test_loc, lines[0], 12);
+                await SetBreakpoint(debugger_test_loc, lines[1], 12);
+
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingValueTypeMembers'); }, 1);",
+                    debugger_test_loc, lines[0], 12, "MethodUpdatingValueTypeMembers");
+
+                var dt = new DateTime(1, 2, 3, 4, 5, 6);
+                await CheckLocals(pause_location, dt);
+
+                // Resume
+                dt = new DateTime(9, 8, 7, 6, 5, 4);
+                pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MethodUpdatingValueTypeMembers");
+                await CheckLocals(pause_location, dt);
+            });
+
+            async Task CheckLocals(JToken pause_location, DateTime dt)
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(locals, new
+                {
+                    obj = TObject("DebuggerTests.ClassForToStringTests"),
+                        vt = TObject("DebuggerTests.StructForToStringTests")
+                }, "locals");
+
+                var obj_props = await GetObjectOnLocals(locals, "obj");
+                {
+                    await CheckProps(obj_props, new
+                    {
+                        DT = TValueType("System.DateTime", dt.ToString())
+                    }, "locals#obj.DT", num_fields : 5);
+
+                    await CheckDateTime(obj_props, "DT", dt);
+                }
+
+                var vt_props = await GetObjectOnLocals(locals, "obj");
+                {
+                    await CheckProps(vt_props, new
+                    {
+                        DT = TValueType("System.DateTime", dt.ToString())
+                    }, "locals#obj.DT", num_fields : 5);
+
+                    await CheckDateTime(vt_props, "DT", dt);
+                }
+            }
+        }
+
+        [Fact]
+        public async Task CheckUpdatedValueTypeLocalsOnResumeAsync()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                var lines = new [] { 211, 213 };
+                await SetBreakpoint(debugger_test_loc, lines[0], 12);
+                await SetBreakpoint(debugger_test_loc, lines[1], 12);
+
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingValueTypeLocalsAsync'); }, 1);",
+                    debugger_test_loc, lines[0], 12, "MoveNext");
+
+                var dt = new DateTime(1, 2, 3, 4, 5, 6);
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckDateTime(locals, "dt", dt);
+
+                // Resume
+                dt = new DateTime(9, 8, 7, 6, 5, 4);
+                pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MoveNext");
+                locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckDateTime(locals, "dt", dt);
+            });
+        }
+
+        [Fact]
+        public async Task CheckUpdatedVTArrayMembersOnResume()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                var lines = new [] { 222, 224 };
+                await SetBreakpoint(debugger_test_loc, lines[0], 12);
+                await SetBreakpoint(debugger_test_loc, lines[1], 12);
+
+                var dt = new DateTime(1, 2, 3, 4, 5, 6);
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method ('[debugger-test] DebuggerTests.ValueTypesTest:MethodUpdatingVTArrayMembers'); }, 1);",
+                    debugger_test_loc, lines[0], 12, "MethodUpdatingVTArrayMembers");
+                await CheckArrayElements(pause_location, dt);
+
+                // Resume
+                dt = new DateTime(9, 8, 7, 6, 5, 4);
+                pause_location = await SendCommandAndCheck(JObject.FromObject(new { }), "Debugger.resume", debugger_test_loc, lines[1], 12, "MethodUpdatingVTArrayMembers");
+                await CheckArrayElements(pause_location, dt);
+            });
+
+            async Task CheckArrayElements(JToken pause_location, DateTime dt)
+            {
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(locals, new
+                {
+                    ssta = TArray("DebuggerTests.StructForToStringTests[]", 1)
+                }, "locals");
+
+                var ssta = await GetObjectOnLocals(locals, "ssta");
+                var sst0 = await GetObjectOnLocals(ssta, "0");
+                await CheckProps(sst0, new
+                {
+                    DT = TValueType("System.DateTime", dt.ToString())
+                }, "dta [0]", num_fields : 5);
+
+                await CheckDateTime(sst0, "DT", dt);
+            }
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsWithStructsStaticAsync(bool use_cfo)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                ctx.UseCallFunctionOnBeforeGetProperties = use_cfo;
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, 54, 12);
+
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method_async (" +
+                    "'[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalStructsStaticAsync'" +
+                    "); }, 1);",
+                    debugger_test_loc, 54, 12, "MoveNext"); //BUG: method name
+
+                var locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+                await CheckProps(locals, new
+                    {
+                        ss_local = TObject("DebuggerTests.ValueTypesTest.SimpleStruct"),
+                            gs_local = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<int>"),
+                            result = TBool(true)
+                    },
+                    "locals#0");
+
+                var dt = new DateTime(2021, 2, 3, 4, 6, 7);
+                // Check ss_local's properties
+                var ss_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "ss_local");
+                await CheckProps(ss_local_props, new
+                {
+                    str_member = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#str_member"),
+                        dt = TValueType("System.DateTime", dt.ToString()),
+                        gs = TValueType("DebuggerTests.ValueTypesTest.GenericStruct<System.DateTime>"),
+                        Kind = TEnum("System.DateTimeKind", "Utc")
+                }, "ss_local");
+
+                {
+                    // Check ss_local.dt
+                    await CheckDateTime(ss_local_props, "dt", dt);
+
+                    // Check ss_local.gs
+                    await CompareObjectPropertiesFor(ss_local_props, "gs",
+                        new
+                        {
+                            StringField = TString("set in MethodWithLocalStructsStaticAsync#SimpleStruct#gs#StringField"),
+                                List = TObject("System.Collections.Generic.List<System.DateTime>"),
+                                Options = TEnum("DebuggerTests.Options", "Option1")
+                        }
+                    );
+                }
+
+                // Check gs_local's properties
+                var gs_local_props = await GetObjectOnFrame(pause_location["callFrames"][0], "gs_local");
+                await CheckProps(gs_local_props, new
+                {
+                    StringField = TString("gs_local#GenericStruct<ValueTypesTest>#StringField"),
+                        List = TObject("System.Collections.Generic.List<int>"),
+                        Options = TEnum("DebuggerTests.Options", "Option2")
+                }, "gs_local");
+
+                // FIXME: check ss_local.gs.List's members
+            });
+        }
+
+        [Theory]
+        [InlineData(134, 12, "MethodWithLocalsForToStringTest", false, false)]
+        [InlineData(144, 12, "MethodWithArgumentsForToStringTest", true, false)]
+        [InlineData(189, 12, "MethodWithArgumentsForToStringTestAsync", true, true)]
+        [InlineData(179, 12, "MethodWithArgumentsForToStringTestAsync", false, true)]
+        public async Task InspectLocalsForToStringDescriptions(int line, int col, string method_name, bool call_other, bool invoke_async)
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+            string entry_method_name = $"[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalsForToStringTest{(invoke_async ? "Async" : String.Empty)}";
+            int frame_idx = 0;
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+                var debugger_test_loc = "dotnet://debugger-test.dll/debugger-valuetypes-test.cs";
+
+                await SetBreakpoint(debugger_test_loc, line, col);
+
+                var eval_expr = "window.setTimeout(function() {" +
+                    (invoke_async ? "invoke_static_method_async (" : "invoke_static_method (") +
+                    $"'{entry_method_name}'," +
+                    (call_other ? "true" : "false") +
+                    "); }, 1);";
+                Console.WriteLine($"{eval_expr}");
+
+                var pause_location = await EvaluateAndCheck(eval_expr, debugger_test_loc, line, col, invoke_async ? "MoveNext" : method_name);
+
+                var dt0 = new DateTime(2020, 1, 2, 3, 4, 5);
+                var dt1 = new DateTime(2010, 5, 4, 3, 2, 1);
+                var ts = dt0 - dt1;
+                var dto = new DateTimeOffset(dt0, new TimeSpan(4, 5, 0));
+
+                var frame_locals = await GetProperties(pause_location["callFrames"][frame_idx]["callFrameId"].Value<string>());
+                await CheckProps(frame_locals, new
+                {
+                    call_other = TBool(call_other),
+                        dt0 = TValueType("System.DateTime", dt0.ToString()),
+                        dt1 = TValueType("System.DateTime", dt1.ToString()),
+                        dto = TValueType("System.DateTimeOffset", dto.ToString()),
+                        ts = TValueType("System.TimeSpan", ts.ToString()),
+                        dec = TValueType("System.Decimal", "123987123"),
+                        guid = TValueType("System.Guid", "3D36E07E-AC90-48C6-B7EC-A481E289D014"),
+                        dts = TArray("System.DateTime[]", 2),
+                        obj = TObject("DebuggerTests.ClassForToStringTests"),
+                        sst = TObject("DebuggerTests.StructForToStringTests")
+                }, "locals#0");
+
+                var dts_0 = new DateTime(1983, 6, 7, 5, 6, 10);
+                var dts_1 = new DateTime(1999, 10, 15, 1, 2, 3);
+                var dts_elements = await GetObjectOnLocals(frame_locals, "dts");
+                await CheckDateTime(dts_elements, "0", dts_0);
+                await CheckDateTime(dts_elements, "1", dts_1);
+
+                // TimeSpan
+                await CompareObjectPropertiesFor(frame_locals, "ts",
+                    new
+                    {
+                        Days = TNumber(3530),
+                            Minutes = TNumber(2),
+                            Seconds = TNumber(4),
+                    }, "ts_props", num_fields : 12);
+
+                // DateTimeOffset
+                await CompareObjectPropertiesFor(frame_locals, "dto",
+                    new
+                    {
+                        Day = TNumber(2),
+                            Year = TNumber(2020),
+                            DayOfWeek = TEnum("System.DayOfWeek", "Thursday")
+                    }, "dto_props", num_fields : 22);
+
+                var DT = new DateTime(2004, 10, 15, 1, 2, 3);
+                var DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0));
+
+                var obj_props = await CompareObjectPropertiesFor(frame_locals, "obj",
+                    new
+                    {
+                        DT = TValueType("System.DateTime", DT.ToString()),
+                            DTO = TValueType("System.DateTimeOffset", DTO.ToString()),
+                            TS = TValueType("System.TimeSpan", ts.ToString()),
+                            Dec = TValueType("System.Decimal", "1239871"),
+                            Guid = TValueType("System.Guid", "3D36E07E-AC90-48C6-B7EC-A481E289D014")
+                    }, "obj_props");
+
+                DTO = new DateTimeOffset(dt0, new TimeSpan(3, 15, 0));
+                var sst_props = await CompareObjectPropertiesFor(frame_locals, "sst",
+                    new
+                    {
+                        DT = TValueType("System.DateTime", DT.ToString()),
+                            DTO = TValueType("System.DateTimeOffset", DTO.ToString()),
+                            TS = TValueType("System.TimeSpan", ts.ToString()),
+                            Dec = TValueType("System.Decimal", "1239871"),
+                            Guid = TValueType("System.Guid", "3D36E07E-AC90-48C6-B7EC-A481E289D014")
+                    }, "sst_props");
+            });
+        }
+
+        [Fact]
+        public async Task InspectLocals()
+        {
+            var insp = new Inspector();
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var wait_res = await RunUntil("locals_inner");
+                var locals = await GetProperties(wait_res["callFrames"][1]["callFrameId"].Value<string>());
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public async Task InspectLocalsForStructInstanceMethod(bool use_cfo) => await CheckInspectLocalsAtBreakpointSite(
+            "dotnet://debugger-test.dll/debugger-array-test.cs", 258, 12,
+            "GenericInstanceMethod<DebuggerTests.SimpleClass>",
+            "window.setTimeout(function() { invoke_static_method_async ('[debugger-test] DebuggerTests.EntryClass:run'); })",
+            use_cfo : use_cfo,
+            wait_for_event_fn : async(pause_location) =>
+            {
+                var frame_locals = await GetProperties(pause_location["callFrames"][0]["callFrameId"].Value<string>());
+
+                await CheckProps(frame_locals, new
+                    {
+                        sc_arg = TObject("DebuggerTests.SimpleClass"),
+                            @this = TValueType("DebuggerTests.Point"),
+                            local_gs = TValueType("DebuggerTests.SimpleGenericStruct<int>")
+                    },
+                    "locals#0");
+
+                await CompareObjectPropertiesFor(frame_locals, "local_gs",
+                    new
+                    {
+                        Id = TString("local_gs#Id"),
+                            Color = TEnum("DebuggerTests.RGB", "Green"),
+                            Value = TNumber(4)
+                    },
+                    label: "local_gs#0");
+
+                await CompareObjectPropertiesFor(frame_locals, "sc_arg",
+                    TSimpleClass(10, 45, "sc_arg#Id", "Blue"),
+                    label: "sc_arg#0");
+
+                await CompareObjectPropertiesFor(frame_locals, "this",
+                    TPoint(90, -4, "point#Id", "Green"),
+                    label: "this#0");
+
+            });
+
+        [Fact]
+        public async Task SteppingIntoMscorlib()
+        {
+            var insp = new Inspector();
+            //Collect events
+            var scripts = SubscribeToScripts(insp);
+
+            await Ready();
+            await insp.Ready(async(cli, token) =>
+            {
+                ctx = new DebugTestContext(cli, insp, token, scripts);
+
+                var bp = await SetBreakpoint("dotnet://debugger-test.dll/debugger-test.cs", 83, 8);
+                var pause_location = await EvaluateAndCheck(
+                    "window.setTimeout(function() { invoke_static_method ('[debugger-test] Math:OuterMethod'); }, 1);",
+                    "dotnet://debugger-test.dll/debugger-test.cs", 83, 8,
+                    "OuterMethod");
+
+                //make sure we're on the right bp
+                Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value<string>());
+
+                pause_location = await SendCommandAndCheck(null, $"Debugger.stepInto", null, -1, -1, null);
+                var top_frame = pause_location["callFrames"][0];
+
+                AssertEqual("WriteLine", top_frame["functionName"]?.Value<string>(), "Expected to be in WriteLine method");
+                var script_id = top_frame["functionLocation"]["scriptId"].Value<string>();
+                AssertEqual("dotnet://System.Console.dll/Console.cs", scripts[script_id], "Expected to stopped in System.Console.WriteLine");
+            });
+        }
+
+        //TODO add tests covering basic stepping behavior as step in/out/over
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json b/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json
new file mode 100644 (file)
index 0000000..0cda8d4
--- /dev/null
@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Error",
+      "Microsoft": "Warning",
+      "Microsoft.Hosting.Lifetime": "Information"
+    }
+  }
+}
diff --git a/src/mono/wasm/debugger/tests/debugger-array-test.cs b/src/mono/wasm/debugger/tests/debugger-array-test.cs
new file mode 100644 (file)
index 0000000..95e2fb1
--- /dev/null
@@ -0,0 +1,308 @@
+// 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;
+namespace DebuggerTests
+{
+    public class ArrayTestsClass
+    {
+        public static void PrimitiveTypeLocals(bool call_other = false)
+        {
+            var int_arr = new int[] { 4, 70, 1 };
+            var int_arr_empty = new int[0];
+            int[] int_arr_null = null;
+
+            if (call_other)
+                OtherMethod();
+
+            Console.WriteLine($"int_arr: {int_arr.Length}, {int_arr_empty.Length}, {int_arr_null?.Length}");
+        }
+
+        public static void ValueTypeLocals(bool call_other = false)
+        {
+            var point_arr = new Point[]
+            {
+            new Point { X = 5, Y = -2, Id = "point_arr#Id#0", Color = RGB.Green },
+            new Point { X = 123, Y = 0, Id = "point_arr#Id#1", Color = RGB.Blue },
+            };
+
+            var point_arr_empty = new Point[0];
+            Point[] point_arr_null = null;
+
+            if (call_other)
+                OtherMethod();
+
+            Console.WriteLine($"point_arr: {point_arr.Length}, {point_arr_empty.Length}, {point_arr_null?.Length}");
+        }
+
+        public static void ObjectTypeLocals(bool call_other = false)
+        {
+            var class_arr = new SimpleClass[]
+            {
+            new SimpleClass { X = 5, Y = -2, Id = "class_arr#Id#0", Color = RGB.Green },
+            null,
+            new SimpleClass { X = 123, Y = 0, Id = "class_arr#Id#2", Color = RGB.Blue },
+            };
+
+            var class_arr_empty = new SimpleClass[0];
+            SimpleClass[] class_arr_null = null;
+
+            if (call_other)
+                OtherMethod();
+
+            Console.WriteLine($"class_arr: {class_arr.Length}, {class_arr_empty.Length}, {class_arr_null?.Length}");
+        }
+
+        public static void GenericTypeLocals(bool call_other = false)
+        {
+            var gclass_arr = new GenericClass<int>[]
+            {
+            null,
+            new GenericClass<int> { Id = "gclass_arr#1#Id", Color = RGB.Red, Value = 5 },
+            new GenericClass<int> { Id = "gclass_arr#2#Id", Color = RGB.Blue, Value = -12 },
+            };
+
+            var gclass_arr_empty = new GenericClass<int>[0];
+            GenericClass<int>[] gclass_arr_null = null;
+
+            if (call_other)
+                OtherMethod();
+
+            Console.WriteLine($"gclass_arr: {gclass_arr.Length}, {gclass_arr_empty.Length}, {gclass_arr_null?.Length}");
+        }
+
+        public static void GenericValueTypeLocals(bool call_other = false)
+        {
+            var gvclass_arr = new SimpleGenericStruct<Point>[]
+            {
+            new SimpleGenericStruct<Point> { Id = "gvclass_arr#1#Id", Color = RGB.Red, Value = new Point { X = 100, Y = 200, Id = "gvclass_arr#1#Value#Id", Color = RGB.Red } },
+            new SimpleGenericStruct<Point> { Id = "gvclass_arr#2#Id", Color = RGB.Blue, Value = new Point { X = 10, Y = 20, Id = "gvclass_arr#2#Value#Id", Color = RGB.Green } }
+            };
+
+            var gvclass_arr_empty = new SimpleGenericStruct<Point>[0];
+            SimpleGenericStruct<Point>[] gvclass_arr_null = null;
+
+            if (call_other)
+                OtherMethod();
+
+            Console.WriteLine($"gvclass_arr: {gvclass_arr.Length}, {gvclass_arr_empty.Length}, {gvclass_arr_null?.Length}");
+        }
+
+        static void OtherMethod()
+        {
+            YetAnotherMethod();
+            Console.WriteLine($"Just a placeholder for breakpoints");
+        }
+
+        static void YetAnotherMethod()
+        {
+            Console.WriteLine($"Just a placeholder for breakpoints");
+        }
+
+        public static void ObjectArrayMembers()
+        {
+            var c = new Container
+            {
+                id = "c#id",
+                ClassArrayProperty = new SimpleClass[]
+                {
+                new SimpleClass { X = 5, Y = -2, Id = "ClassArrayProperty#Id#0", Color = RGB.Green },
+                new SimpleClass { X = 30, Y = 1293, Id = "ClassArrayProperty#Id#1", Color = RGB.Green },
+                null
+                },
+                ClassArrayField = new SimpleClass[]
+                {
+                null,
+                new SimpleClass { X = 5, Y = -2, Id = "ClassArrayField#Id#1", Color = RGB.Blue },
+                new SimpleClass { X = 30, Y = 1293, Id = "ClassArrayField#Id#2", Color = RGB.Green },
+                },
+                PointsProperty = new Point[]
+                {
+                new Point { X = 5, Y = -2, Id = "PointsProperty#Id#0", Color = RGB.Green },
+                new Point { X = 123, Y = 0, Id = "PointsProperty#Id#1", Color = RGB.Blue },
+                },
+                PointsField = new Point[]
+                {
+                new Point { X = 5, Y = -2, Id = "PointsField#Id#0", Color = RGB.Green },
+                new Point { X = 123, Y = 0, Id = "PointsField#Id#1", Color = RGB.Blue },
+                }
+            };
+
+            Console.WriteLine($"Back from PlaceholderMethod, {c.ClassArrayProperty?.Length}");
+            c.PlaceholderMethod();
+            Console.WriteLine($"Back from PlaceholderMethod, {c.id}");
+        }
+
+        public static async Task<bool> ValueTypeLocalsAsync(bool call_other = false)
+        {
+            var gvclass_arr = new SimpleGenericStruct<Point>[]
+            {
+            new SimpleGenericStruct<Point> { Id = "gvclass_arr#1#Id", Color = RGB.Red, Value = new Point { X = 100, Y = 200, Id = "gvclass_arr#1#Value#Id", Color = RGB.Red } },
+            new SimpleGenericStruct<Point> { Id = "gvclass_arr#2#Id", Color = RGB.Blue, Value = new Point { X = 10, Y = 20, Id = "gvclass_arr#2#Value#Id", Color = RGB.Green } }
+            };
+
+            var gvclass_arr_empty = new SimpleGenericStruct<Point>[0];
+            SimpleGenericStruct<Point>[] gvclass_arr_null = null;
+            Console.WriteLine($"ValueTypeLocalsAsync: call_other: {call_other}");
+            SimpleGenericStruct<Point> gvclass;
+            Point[] points = null;
+
+            if (call_other)
+            {
+                (gvclass, points) = await new ArrayTestsClass().InstanceMethodValueTypeLocalsAsync<SimpleGenericStruct<Point>>(gvclass_arr[0]);
+                Console.WriteLine($"* gvclass: {gvclass}, points: {points.Length}");
+            }
+
+            Console.WriteLine($"gvclass_arr: {gvclass_arr.Length}, {gvclass_arr_empty.Length}, {gvclass_arr_null?.Length}");
+            return true;
+        }
+
+        public async Task < (T, Point[]) > InstanceMethodValueTypeLocalsAsync<T>(T t1)
+        {
+            var point_arr = new Point[]
+            {
+                new Point { X = 5, Y = -2, Id = "point_arr#Id#0", Color = RGB.Red },
+                new Point { X = 123, Y = 0, Id = "point_arr#Id#1", Color = RGB.Blue }
+            };
+            var point = new Point { X = 45, Y = 51, Id = "point#Id", Color = RGB.Green };
+
+            Console.WriteLine($"point_arr: {point_arr.Length}, T: {t1}, point: {point}");
+            return (t1, new Point[] { point_arr[0], point_arr[1], point });
+        }
+
+        // A workaround for method invocations on structs not working right now
+        public static async Task EntryPointForStructMethod(bool call_other = false)
+        {
+            await Point.AsyncMethod(call_other);
+        }
+
+        public static void GenericValueTypeLocals2(bool call_other = false)
+        {
+            var gvclass_arr = new SimpleGenericStruct<Point[]>[]
+            {
+            new SimpleGenericStruct<Point[]>
+            {
+            Id = "gvclass_arr#0#Id",
+            Color = RGB.Red,
+            Value = new Point[]
+            {
+            new Point { X = 100, Y = 200, Id = "gvclass_arr#0#0#Value#Id", Color = RGB.Red },
+            new Point { X = 100, Y = 200, Id = "gvclass_arr#0#1#Value#Id", Color = RGB.Green }
+            }
+            },
+
+            new SimpleGenericStruct<Point[]>
+            {
+            Id = "gvclass_arr#1#Id",
+            Color = RGB.Blue,
+            Value = new Point[]
+            {
+            new Point { X = 100, Y = 200, Id = "gvclass_arr#1#0#Value#Id", Color = RGB.Green },
+            new Point { X = 100, Y = 200, Id = "gvclass_arr#1#1#Value#Id", Color = RGB.Blue }
+            }
+            },
+            };
+
+            var gvclass_arr_empty = new SimpleGenericStruct<Point[]>[0];
+            SimpleGenericStruct<Point[]>[] gvclass_arr_null = null;
+
+            if (call_other)
+                OtherMethod();
+
+            Console.WriteLine($"gvclass_arr: {gvclass_arr.Length}, {gvclass_arr_empty.Length}, {gvclass_arr_null?.Length}");
+        }
+    }
+
+    public class Container
+    {
+        public string id;
+        public SimpleClass[] ClassArrayProperty { get; set; }
+        public SimpleClass[] ClassArrayField;
+
+        public Point[] PointsProperty { get; set; }
+        public Point[] PointsField;
+
+        public void PlaceholderMethod()
+        {
+            Console.WriteLine($"Container.PlaceholderMethod");
+        }
+    }
+
+    public struct Point
+    {
+        public int X, Y;
+        public string Id { get; set; }
+        public RGB Color { get; set; }
+
+        /* instance too */
+        public static async Task AsyncMethod(bool call_other)
+        {
+            int local_i = 5;
+            var sc = new SimpleClass { X = 10, Y = 45, Id = "sc#Id", Color = RGB.Blue };
+            if (call_other)
+                await new Point { X = 90, Y = -4, Id = "point#Id", Color = RGB.Green }.AsyncInstanceMethod(sc);
+            Console.WriteLine($"AsyncMethod local_i: {local_i}, sc: {sc.Id}");
+        }
+
+        public async Task AsyncInstanceMethod(SimpleClass sc_arg)
+        {
+            var local_gs = new SimpleGenericStruct<int> { Id = "local_gs#Id", Color = RGB.Green, Value = 4 };
+            sc_arg.Id = "sc_arg#Id";
+            Console.WriteLine($"AsyncInstanceMethod sc_arg: {sc_arg.Id}, local_gs: {local_gs.Id}");
+        }
+
+        public void GenericInstanceMethod<T>(T sc_arg) where T : SimpleClass
+        {
+            var local_gs = new SimpleGenericStruct<int> { Id = "local_gs#Id", Color = RGB.Green, Value = 4 };
+            sc_arg.Id = "sc_arg#Id";
+            Console.WriteLine($"AsyncInstanceMethod sc_arg: {sc_arg.Id}, local_gs: {local_gs.Id}");
+        }
+    }
+
+    public class SimpleClass
+    {
+        public int X, Y;
+        public string Id { get; set; }
+        public RGB Color { get; set; }
+
+        public Point PointWithCustomGetter { get { return new Point { X = 100, Y = 400, Id = "SimpleClass#Point#gen#Id", Color = RGB.Green }; } }
+    }
+
+    public class GenericClass<T>
+    {
+        public string Id { get; set; }
+        public RGB Color { get; set; }
+        public T Value { get; set; }
+    }
+
+    public struct SimpleGenericStruct<T>
+    {
+        public string Id { get; set; }
+        public RGB Color { get; set; }
+        public T Value { get; set; }
+    }
+
+    public class EntryClass
+    {
+        public static void run()
+        {
+            ArrayTestsClass.PrimitiveTypeLocals(true);
+            ArrayTestsClass.ValueTypeLocals(true);
+            ArrayTestsClass.ObjectTypeLocals(true);
+
+            ArrayTestsClass.GenericTypeLocals(true);
+            ArrayTestsClass.GenericValueTypeLocals(true);
+            ArrayTestsClass.GenericValueTypeLocals2(true);
+
+            ArrayTestsClass.ObjectArrayMembers();
+
+            ArrayTestsClass.ValueTypeLocalsAsync(true).Wait();
+
+            ArrayTestsClass.EntryPointForStructMethod(true).Wait();
+
+            var sc = new SimpleClass { X = 10, Y = 45, Id = "sc#Id", Color = RGB.Blue };
+            new Point { X = 90, Y = -4, Id = "point#Id", Color = RGB.Green }.GenericInstanceMethod(sc);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-cfo-test.cs b/src/mono/wasm/debugger/tests/debugger-cfo-test.cs
new file mode 100644 (file)
index 0000000..ad3b12c
--- /dev/null
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace DebuggerTests
+{
+    public class CallFunctionOnTest
+    {
+        public static void LocalsTest(int len)
+        {
+            var big = new int[len];
+            for (int i = 0; i < len; i++)
+                big[i] = i + 1000;
+
+            var simple_struct = new Math.SimpleStruct() { dt = new DateTime(2020, 1, 2, 3, 4, 5), gs = new Math.GenericStruct<DateTime> { StringField = $"simple_struct # gs # StringField" } };
+
+            var ss_arr = new Math.SimpleStruct[len];
+            for (int i = 0; i < len; i++)
+                ss_arr[i] = new Math.SimpleStruct() { dt = new DateTime(2020 + i, 1, 2, 3, 4, 5), gs = new Math.GenericStruct<DateTime> { StringField = $"ss_arr # {i} # gs # StringField" } };
+
+            var nim = new Math.NestedInMath { SimpleStructProperty = new Math.SimpleStruct() { dt = new DateTime(2010, 6, 7, 8, 9, 10) } };
+            Action<Math.GenericStruct<int[]>> action = Math.DelegateTargetWithVoidReturn;
+            Console.WriteLine("foo");
+        }
+
+        public static void PropertyGettersTest()
+        {
+            var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) };
+            var swp = new StructWithProperties();
+            System.Console.WriteLine("break here");
+        }
+
+        public static async System.Threading.Tasks.Task PropertyGettersTestAsync()
+        {
+            var ptd = new ClassWithProperties { DTAutoProperty = new DateTime(4, 5, 6, 7, 8, 9) };
+            var swp = new StructWithProperties();
+            System.Console.WriteLine("break here");
+            await System.Threading.Tasks.Task.CompletedTask;
+        }
+    }
+
+    class ClassWithProperties
+    {
+        public int Int { get { return 5; } }
+        public string String { get { return "foobar"; } }
+        public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } }
+
+        public int[] IntArray { get { return new int[] { 10, 20 }; } }
+        public DateTime[] DTArray { get { return new DateTime[] { new DateTime(6, 7, 8, 9, 10, 11), new DateTime(1, 2, 3, 4, 5, 6) }; } }
+        public DateTime DTAutoProperty { get; set; }
+        public string StringField;
+    }
+
+    struct StructWithProperties
+    {
+        public int Int { get { return 5; } }
+        public string String { get { return "foobar"; } }
+        public DateTime DT { get { return new DateTime(3, 4, 5, 6, 7, 8); } }
+
+        public int[] IntArray { get { return new int[] { 10, 20 }; } }
+        public DateTime[] DTArray { get { return new DateTime[] { new DateTime(6, 7, 8, 9, 10, 11), new DateTime(1, 2, 3, 4, 5, 6) }; } }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-datetime-test.cs b/src/mono/wasm/debugger/tests/debugger-datetime-test.cs
new file mode 100644 (file)
index 0000000..8ea2650
--- /dev/null
@@ -0,0 +1,29 @@
+// 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.Globalization;
+namespace DebuggerTests
+{
+    public class DateTimeTest
+    {
+        public static string LocaleTest(string locale)
+        {
+            CultureInfo.CurrentCulture = new CultureInfo(locale, false);
+            Console.WriteLine("CurrentCulture is {0}", CultureInfo.CurrentCulture.Name);
+
+            DateTimeFormatInfo dtfi = CultureInfo.GetCultureInfo(locale).DateTimeFormat;
+            var fdtp = dtfi.FullDateTimePattern;
+            var ldp = dtfi.LongDatePattern;
+            var ltp = dtfi.LongTimePattern;
+            var sdp = dtfi.ShortDatePattern;
+            var stp = dtfi.ShortTimePattern;
+
+            DateTime dt = new DateTime(2020, 1, 2, 3, 4, 5);
+            string dt_str = dt.ToString();
+            Console.WriteLine("Current time is {0}", dt_str);
+
+            return dt_str;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-driver.html b/src/mono/wasm/debugger/tests/debugger-driver.html
new file mode 100644 (file)
index 0000000..049b6b6
--- /dev/null
@@ -0,0 +1,124 @@
+<!doctype html>
+<html lang="en-us">
+  <head>
+  </head>
+  <body>
+       <script type='text/javascript'>
+               var App = {
+                       init: function () {
+                               this.int_add = Module.mono_bind_static_method ("[debugger-test] Math:IntAdd");
+                               this.use_complex = Module.mono_bind_static_method ("[debugger-test] Math:UseComplex");
+                               this.delegates_test = Module.mono_bind_static_method ("[debugger-test] Math:DelegatesTest");
+                               this.generic_types_test = Module.mono_bind_static_method ("[debugger-test] Math:GenericTypesTest");
+                               this.outer_method = Module.mono_bind_static_method ("[debugger-test] Math:OuterMethod");
+                               this.async_method = Module.mono_bind_static_method ("[debugger-test] Math/NestedInMath:AsyncTest");
+                               this.method_with_structs = Module.mono_bind_static_method ("[debugger-test] DebuggerTests.ValueTypesTest:MethodWithLocalStructs");
+                               this.run_all = Module.mono_bind_static_method ("[debugger-test] DebuggerTest:run_all");
+                               this.static_method_table = {};
+                               console.log ("ready");
+                       },
+               };
+               function invoke_static_method (method_name, ...args) {
+                       var method = App.static_method_table [method_name];
+                       if (method == undefined)
+                               method = App.static_method_table [method_name] = Module.mono_bind_static_method (method_name);
+
+                       return method (...args);
+               }
+
+               async function invoke_static_method_async (method_name, ...args) {
+                       var method = App.static_method_table [method_name];
+                       if (method == undefined) {
+                               method = App.static_method_table [method_name] = Module.mono_bind_static_method (method_name);
+                       }
+
+                       return await method (...args);
+               }
+
+               function invoke_big_array_js_test (len) {
+                       big_array_js_test(len);
+               }
+
+               function invoke_getters_js_test () {
+                       getters_js_test ();
+               }
+
+               function invoke_add () {
+                       return App.int_add (10, 20);
+               }
+               function invoke_use_complex () {
+                       return App.use_complex (10, 20);
+               }
+               function invoke_delegates_test () {
+                       return App.delegates_test ();
+               }
+               function invoke_generic_types_test () {
+                       return App.generic_types_test ();
+               }
+               function invoke_bad_js_test () {
+                       console.log ("js: In invoke_bad_js_test");
+                       App.non_existant ();
+                       console.log ("js: After.. shouldn't reach here");
+               }
+               function invoke_outer_method () {
+                       console.log('invoke_outer_method called');
+                       return App.outer_method ();
+               }
+               async function invoke_async_method_with_await () {
+                       return await App.async_method ("string from js", 42);
+               }
+               function invoke_method_with_structs () {
+                       return App.method_with_structs ();
+               }
+               function invoke_run_all () {
+                       return App.run_all ();
+               }
+      </script>
+      <script type="text/javascript" src="mono-config.js"></script>
+      <script type="text/javascript" src="runtime-debugger.js"></script>
+      <script type="text/javascript" src="other.js"></script>
+      <script async type="text/javascript" src="dotnet.js"></script>
+         Stuff goes here
+  </body>
+  </html> 
+     
+17  sdks/wasm/debugger-test.cs
+@@ -0,0 +1,17 @@
+using System;
+
+public class Math { //Only append content to this class as the test suite depends on line info
+       public static int IntAdd (int a, int b) {
+               int c = a + b;
+               int d = c + b;
+               int e = d + a;
+
+               return e;
+       }
+
+       public static int UseComplex () {
+               var complex = new Simple.Complex (10, "xx");
+               var res = complex.DoStuff ();
+               return res;
+       }
+}
+     
+19  sdks/wasm/debugger-test2.cs
+@@ -0,0 +1,19 @@
+using System;
+
+public class Misc { //Only append content to this class as the test suite depends on line info
+       public static int CreateObject (int foo, int bar) {
+               var f = new Fancy () {
+                       Foo = foo,
+                       Bar = bar,
+               };
+
+               Console.WriteLine ($"{f.Foo} {f.Bar}");
+               return f.Foo + f.Bar;
+       }
+}
+
+public class Fancy {
+       public int Foo;
+       public int Bar { get ; set; }
+}
diff --git a/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs b/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs
new file mode 100644 (file)
index 0000000..4d958cd
--- /dev/null
@@ -0,0 +1,108 @@
+// 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;
+namespace DebuggerTests
+{
+    public class EvaluateTestsClass
+    {
+        public class TestEvaluate
+        {
+            public int a;
+            public int b;
+            public int c;
+            public DateTime dt = new DateTime(2000, 5, 4, 3, 2, 1);
+            public void run(int g, int h, string valString)
+            {
+                int d = g + 1;
+                int e = g + 2;
+                int f = g + 3;
+                int i = d + e + f;
+                var local_dt = new DateTime(2010, 9, 8, 7, 6, 5);
+                a = 1;
+                b = 2;
+                c = 3;
+                a = a + 1;
+                b = b + 1;
+                c = c + 1;
+            }
+        }
+
+        public static void EvaluateLocals()
+        {
+            TestEvaluate f = new TestEvaluate();
+            f.run(100, 200, "test");
+
+            var f_s = new EvaluateTestsStruct();
+            f_s.EvaluateTestsStructInstanceMethod(100, 200, "test");
+            f_s.GenericInstanceMethodOnStruct<int>(100, 200, "test");
+
+            var f_g_s = new EvaluateTestsGenericStruct<int>();
+            f_g_s.EvaluateTestsGenericStructInstanceMethod(100, 200, "test");
+            Console.WriteLine($"a: {f.a}, b: {f.b}, c: {f.c}");
+        }
+
+    }
+
+    public struct EvaluateTestsStruct
+    {
+        public int a;
+        public int b;
+        public int c;
+        DateTime dateTime;
+        public void EvaluateTestsStructInstanceMethod(int g, int h, string valString)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            int i = d + e + f;
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
+        }
+
+        public void GenericInstanceMethodOnStruct<T>(int g, int h, string valString)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            int i = d + e + f;
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            T t = default(T);
+            a = a + 1;
+            b = b + 1;
+            c = c + 1;
+        }
+    }
+
+    public struct EvaluateTestsGenericStruct<T>
+    {
+        public int a;
+        public int b;
+        public int c;
+        DateTime dateTime;
+        public void EvaluateTestsGenericStructInstanceMethod(int g, int h, string valString)
+        {
+            int d = g + 1;
+            int e = g + 2;
+            int f = g + 3;
+            int i = d + e + f;
+            a = 1;
+            b = 2;
+            c = 3;
+            dateTime = new DateTime(2020, 1, 2, 3, 4, 5);
+            T t = default(T);
+            a = a + 1;
+            b = b + 2;
+            c = c + 3;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-pointers-test.cs b/src/mono/wasm/debugger/tests/debugger-pointers-test.cs
new file mode 100644 (file)
index 0000000..122a080
--- /dev/null
@@ -0,0 +1,112 @@
+// 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;
+
+namespace DebuggerTests
+{
+    public class PointerTests
+    {
+
+        public static unsafe void LocalPointers()
+        {
+            int ivalue0 = 5;
+            int ivalue1 = 10;
+
+            int* ip = &ivalue0;
+            int* ip_null = null;
+            int** ipp = &ip;
+            int** ipp_null = &ip_null;
+            int*[] ipa = new int*[] { &ivalue0, &ivalue1, null };
+            int**[] ippa = new int**[] { &ip, &ip_null, ipp, ipp_null, null };
+            char cvalue0 = 'q';
+            char* cp = &cvalue0;
+
+            DateTime dt = new DateTime(5, 6, 7, 8, 9, 10);
+            void* vp = &dt;
+            void* vp_null = null;
+            void** vpp = &vp;
+            void** vpp_null = &vp_null;
+
+            DateTime* dtp = &dt;
+            DateTime* dtp_null = null;
+            DateTime*[] dtpa = new DateTime*[] { dtp, dtp_null };
+            DateTime**[] dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+            Console.WriteLine($"-- break here: ip_null==null: {ip_null == null}, ipp_null: {ipp_null == null}, *ipp_null==ip_null: {*ipp_null == ip_null}, *ipp_null==null: {*ipp_null == null}");
+
+            var gs = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp };
+            var gs_null = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp_null };
+            var gsp = &gs;
+            var gsp_null = &gs_null;
+            var gspa = new GenericStructWithUnmanagedT<DateTime>*[] { null, gsp, gsp_null };
+
+            var cwp = new GenericClassWithPointers<DateTime> { Ptr = dtp };
+            var cwp_null = new GenericClassWithPointers<DateTime>();
+            Console.WriteLine($"{(int)*ip}, {(int)**ipp}, {ipp_null == null}, {ip_null == null}, {ippa == null}, {ipa}, {(char)*cp}, {(vp == null ? "null" : "not null")}, {dtp->Second}, {gsp->IntField}, {cwp}, {cwp_null}, {gs_null}");
+
+            PointersAsArgsTest(ip, ipp, ipa, ippa, &dt, &dtp, dtpa, dtppa);
+        }
+
+        static unsafe void  PointersAsArgsTest(int* ip, int** ipp, int*[] ipa, int**[] ippa,
+                            DateTime* dtp, DateTime** dtpp, DateTime*[] dtpa, DateTime**[] dtppa)
+        {
+            Console.WriteLine($"break here!");
+            if (ip == null)
+                Console.WriteLine($"ip is null");
+            Console.WriteLine($"done!");
+        }
+
+        public static unsafe async Task LocalPointersAsync()
+        {
+            int ivalue0 = 5;
+            int ivalue1 = 10;
+
+            int* ip = &ivalue0;
+            int* ip_null = null;
+            int** ipp = &ip;
+            int** ipp_null = &ip_null;
+            int*[] ipa = new int*[] { &ivalue0, &ivalue1, null };
+            int**[] ippa = new int**[] { &ip, &ip_null, ipp, ipp_null, null };
+            char cvalue0 = 'q';
+            char* cp = &cvalue0;
+
+            DateTime dt = new DateTime(5, 6, 7, 8, 9, 10);
+            void* vp = &dt;
+            void* vp_null = null;
+            void** vpp = &vp;
+            void** vpp_null = &vp_null;
+
+            DateTime* dtp = &dt;
+            DateTime* dtp_null = null;
+            DateTime*[] dtpa = new DateTime*[] { dtp, dtp_null };
+            DateTime**[] dtppa = new DateTime**[] { &dtp, &dtp_null, null };
+            Console.WriteLine($"-- break here: ip_null==null: {ip_null == null}, ipp_null: {ipp_null == null}, *ipp_null==ip_null: {*ipp_null == ip_null}, *ipp_null==null: {*ipp_null == null}");
+
+            var gs = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp };
+            var gs_null = new GenericStructWithUnmanagedT<DateTime> { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp_null };
+            var gsp = &gs;
+            var gsp_null = &gs_null;
+            var gspa = new GenericStructWithUnmanagedT<DateTime>*[] { null, gsp, gsp_null };
+
+            var cwp = new GenericClassWithPointers<DateTime> { Ptr = dtp };
+            var cwp_null = new GenericClassWithPointers<DateTime>();
+            Console.WriteLine($"{(int)*ip}, {(int)**ipp}, {ipp_null == null}, {ip_null == null}, {ippa == null}, {ipa}, {(char)*cp}, {(vp == null ? "null" : "not null")}, {dtp->Second}, {gsp->IntField}, {cwp}, {cwp_null}, {gs_null}");
+        }
+
+        // async methods cannot have unsafe params, so no test for that
+    }
+
+    public unsafe struct GenericStructWithUnmanagedT<T> where T : unmanaged
+    {
+        public T Value;
+        public int IntField;
+
+        public DateTime** DTPP;
+    }
+
+    public unsafe class GenericClassWithPointers<T> where T : unmanaged
+    {
+        public unsafe T* Ptr;
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-test.cs b/src/mono/wasm/debugger/tests/debugger-test.cs
new file mode 100644 (file)
index 0000000..55a13d5
--- /dev/null
@@ -0,0 +1,325 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public partial class Math
+{ //Only append content to this class as the test suite depends on line info
+    public static int IntAdd(int a, int b)
+    {
+        int c = a + b;
+        int d = c + b;
+        int e = d + a;
+        int f = 0;
+        return e;
+    }
+
+    public static int UseComplex(int a, int b)
+    {
+        var complex = new Simple.Complex(10, "xx");
+        int c = a + b;
+        int d = c + b;
+        int e = d + a;
+        int f = 0;
+        e += complex.DoStuff();
+        return e;
+    }
+
+    delegate bool IsMathNull(Math m);
+
+    public static int DelegatesTest()
+    {
+        Func<Math, bool> fn_func = (Math m) => m == null;
+        Func<Math, bool> fn_func_null = null;
+        Func<Math, bool>[] fn_func_arr = new Func<Math, bool>[] {
+            (Math m) => m == null };
+
+        Math.IsMathNull fn_del = Math.IsMathNullDelegateTarget;
+        var fn_del_arr = new Math.IsMathNull[] { Math.IsMathNullDelegateTarget };
+        var m_obj = new Math();
+        Math.IsMathNull fn_del_null = null;
+        bool res = fn_func(m_obj) && fn_del(m_obj) && fn_del_arr[0](m_obj) && fn_del_null == null && fn_func_null == null && fn_func_arr[0] != null;
+
+        // Unused locals
+
+        Func<Math, bool> fn_func_unused = (Math m) => m == null;
+        Func<Math, bool> fn_func_null_unused = null;
+        Func<Math, bool>[] fn_func_arr_unused = new Func<Math, bool>[] { (Math m) => m == null };
+
+        Math.IsMathNull fn_del_unused = Math.IsMathNullDelegateTarget;
+        Math.IsMathNull fn_del_null_unused = null;
+        var fn_del_arr_unused = new Math.IsMathNull[] { Math.IsMathNullDelegateTarget };
+        OuterMethod();
+        Console.WriteLine("Just a test message, ignore");
+        return res ? 0 : 1;
+    }
+
+    public static int GenericTypesTest()
+    {
+        var list = new System.Collections.Generic.Dictionary<Math[], IsMathNull>();
+        System.Collections.Generic.Dictionary<Math[], IsMathNull> list_null = null;
+
+        var list_arr = new System.Collections.Generic.Dictionary<Math[], IsMathNull>[] { new System.Collections.Generic.Dictionary<Math[], IsMathNull>() };
+        System.Collections.Generic.Dictionary<Math[], IsMathNull>[] list_arr_null = null;
+
+        Console.WriteLine($"list_arr.Length: {list_arr.Length}, list.Count: {list.Count}");
+
+        // Unused locals
+
+        var list_unused = new System.Collections.Generic.Dictionary<Math[], IsMathNull>();
+        System.Collections.Generic.Dictionary<Math[], IsMathNull> list_null_unused = null;
+
+        var list_arr_unused = new System.Collections.Generic.Dictionary<Math[], IsMathNull>[] { new System.Collections.Generic.Dictionary<Math[], IsMathNull>() };
+        System.Collections.Generic.Dictionary<Math[], IsMathNull>[] list_arr_null_unused = null;
+
+        OuterMethod();
+        Console.WriteLine("Just a test message, ignore");
+        return 0;
+    }
+
+    static bool IsMathNullDelegateTarget(Math m) => m == null;
+
+    public static void OuterMethod()
+    {
+        Console.WriteLine($"OuterMethod called");
+        var nim = new Math.NestedInMath();
+        var i = 5;
+        var text = "Hello";
+        var new_i = nim.InnerMethod(i);
+        Console.WriteLine($"i: {i}");
+        Console.WriteLine($"-- InnerMethod returned: {new_i}, nim: {nim}, text: {text}");
+        int k = 19;
+        new_i = InnerMethod2("test string", new_i, out k);
+        Console.WriteLine($"-- InnerMethod2 returned: {new_i}, and k: {k}");
+    }
+
+    static int InnerMethod2(string s, int i, out int k)
+    {
+        k = i + 10;
+        Console.WriteLine($"s: {s}, i: {i}, k: {k}");
+        return i - 2;
+    }
+
+    public class NestedInMath
+    {
+        public int InnerMethod(int i)
+        {
+            SimpleStructProperty = new SimpleStruct() { dt = new DateTime(2020, 1, 2, 3, 4, 5) };
+            int j = i + 10;
+            string foo_str = "foo";
+            Console.WriteLine($"i: {i} and j: {j}, foo_str: {foo_str} ");
+            j += 9;
+            Console.WriteLine($"i: {i} and j: {j}");
+            return j;
+        }
+
+        Math m = new Math();
+        public async System.Threading.Tasks.Task<bool> AsyncMethod0(string s, int i)
+        {
+            string local0 = "value0";
+            await System.Threading.Tasks.Task.Delay(1);
+            Console.WriteLine($"* time for the second await, local0: {local0}");
+            await AsyncMethodNoReturn();
+            return true;
+        }
+
+        public async System.Threading.Tasks.Task AsyncMethodNoReturn()
+        {
+            var ss = new SimpleStruct() { dt = new DateTime(2020, 1, 2, 3, 4, 5) };
+            var ss_arr = new SimpleStruct[] { };
+            //ss.gs.StringField = "field in GenericStruct";
+
+            //Console.WriteLine ($"Using the struct: {ss.dt}, {ss.gs.StringField}, ss_arr: {ss_arr.Length}");
+            string str = "AsyncMethodNoReturn's local";
+            //Console.WriteLine ($"* field m: {m}");
+            await System.Threading.Tasks.Task.Delay(1);
+            Console.WriteLine($"str: {str}");
+        }
+
+        public static async System.Threading.Tasks.Task<bool> AsyncTest(string s, int i)
+        {
+            var li = 10 + i;
+            var ls = s + "test";
+            return await new NestedInMath().AsyncMethod0(s, i);
+        }
+
+        public SimpleStruct SimpleStructProperty { get; set; }
+    }
+
+    public static void PrimitiveTypesTest()
+    {
+        char c0 = '€';
+        char c1 = 'A';
+        // TODO: other types!
+        // just trying to ensure vars don't get optimized out
+        if (c0 < 32 || c1 > 32)
+            Console.WriteLine($"{c0}, {c1}");
+    }
+
+    public static int DelegatesSignatureTest()
+    {
+        Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>> fn_func = (m, gs) => new GenericStruct<bool[]>();
+        Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>> fn_func_del = GenericStruct<int>.DelegateTargetForSignatureTest;
+        Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>> fn_func_null = null;
+        Func<bool> fn_func_only_ret = () => { Console.WriteLine ($"hello"); return true; };
+        var fn_func_arr = new Func<Math, GenericStruct<GenericStruct<int[]>>, GenericStruct<bool[]>>[] {
+                (m, gs) => new GenericStruct<bool[]> () };
+
+        Math.DelegateForSignatureTest fn_del = GenericStruct<int>.DelegateTargetForSignatureTest;
+        Math.DelegateForSignatureTest fn_del_l = (m, gs) => new GenericStruct<bool[]> { StringField = "fn_del_l#lambda" };
+        var fn_del_arr = new Math.DelegateForSignatureTest[] { GenericStruct<int>.DelegateTargetForSignatureTest, (m, gs) => new GenericStruct<bool[]> { StringField = "fn_del_arr#1#lambda" } };
+        var m_obj = new Math();
+        Math.DelegateForSignatureTest fn_del_null = null;
+        var gs_gs = new GenericStruct<GenericStruct<int[]>>
+        {
+            List = new System.Collections.Generic.List<GenericStruct<int[]>>
+            {
+            new GenericStruct<int[]> { StringField = "gs#List#0#StringField" },
+            new GenericStruct<int[]> { StringField = "gs#List#1#StringField" }
+            }
+        };
+
+        Math.DelegateWithVoidReturn fn_void_del = Math.DelegateTargetWithVoidReturn;
+        var fn_void_del_arr = new Math.DelegateWithVoidReturn[] { Math.DelegateTargetWithVoidReturn };
+        Math.DelegateWithVoidReturn fn_void_del_null = null;
+
+        var rets = new GenericStruct<bool[]>[]
+        {
+            fn_func(m_obj, gs_gs),
+            fn_func_del(m_obj, gs_gs),
+            fn_del(m_obj, gs_gs),
+            fn_del_l(m_obj, gs_gs),
+            fn_del_arr[0](m_obj, gs_gs),
+            fn_func_arr[0](m_obj, gs_gs)
+        };
+
+        var gs = new GenericStruct<int[]>();
+        fn_void_del(gs);
+        fn_void_del_arr[0](gs);
+        fn_func_only_ret();
+        foreach (var ret in rets) Console.WriteLine($"ret: {ret}");
+        OuterMethod();
+        Console.WriteLine($"- {gs_gs.List[0].StringField}");
+        return 0;
+    }
+
+    public static int ActionTSignatureTest()
+    {
+        Action<GenericStruct<int[]>> fn_action = (_) => { };
+        Action<GenericStruct<int[]>> fn_action_del = Math.DelegateTargetWithVoidReturn;
+        Action fn_action_bare = () => { };
+        Action<GenericStruct<int[]>> fn_action_null = null;
+        var fn_action_arr = new Action<GenericStruct<int[]>>[]
+        {
+            (gs) => new GenericStruct<int[]>(),
+            Math.DelegateTargetWithVoidReturn,
+            null
+        };
+
+        var gs = new GenericStruct<int[]>();
+        fn_action(gs);
+        fn_action_del(gs);
+        fn_action_arr[0](gs);
+        fn_action_bare();
+        OuterMethod();
+        return 0;
+    }
+
+    public static int NestedDelegatesTest()
+    {
+        Func<Func<int, bool>, bool> fn_func = (_) => { return true; };
+        Func<Func<int, bool>, bool> fn_func_null = null;
+        var fn_func_arr = new Func<Func<int, bool>, bool>[] {
+                (gs) => { return true; } };
+
+        var fn_del_arr = new Func<Func<int, bool>, bool>[] { DelegateTargetForNestedFunc<Func<int, bool>> };
+        var m_obj = new Math();
+        Func<Func<int, bool>, bool> fn_del_null = null;
+        Func<int, bool> fs = (i) => i == 0;
+        fn_func(fs);
+        fn_del_arr[0](fs);
+        fn_func_arr[0](fs);
+        OuterMethod();
+        return 0;
+    }
+
+    public static void DelegatesAsMethodArgsTest()
+    {
+        var _dst_arr = new DelegateForSignatureTest[]
+        {
+            GenericStruct<int>.DelegateTargetForSignatureTest,
+            (m, gs) => new GenericStruct<bool[]>()
+        };
+        Func<char[], bool> _fn_func = (cs) => cs.Length == 0;
+        Action<GenericStruct<int>[]> _fn_action = (gss) => { };
+
+        new Math().MethodWithDelegateArgs(_dst_arr, _fn_func, _fn_action);
+    }
+
+    void MethodWithDelegateArgs(Math.DelegateForSignatureTest[] dst_arr, Func<char[], bool> fn_func,
+        Action<GenericStruct<int>[]> fn_action)
+    {
+        Console.WriteLine($"Placeholder for breakpoint");
+        OuterMethod();
+    }
+
+    public static async System.Threading.Tasks.Task MethodWithDelegatesAsyncTest()
+    {
+        await new Math().MethodWithDelegatesAsync();
+    }
+
+    async System.Threading.Tasks.Task MethodWithDelegatesAsync()
+    {
+        var _dst_arr = new DelegateForSignatureTest[]
+        {
+            GenericStruct<int>.DelegateTargetForSignatureTest,
+            (m, gs) => new GenericStruct<bool[]>()
+        };
+        Func<char[], bool> _fn_func = (cs) => cs.Length == 0;
+        Action<GenericStruct<int>[]> _fn_action = (gss) => { };
+
+        Console.WriteLine($"Placeholder for breakpoint");
+        await System.Threading.Tasks.Task.CompletedTask;
+    }
+
+    public delegate void DelegateWithVoidReturn(GenericStruct<int[]> gs);
+    public static void DelegateTargetWithVoidReturn(GenericStruct<int[]> gs) { }
+
+    delegate GenericStruct<bool[]> DelegateForSignatureTest(Math m, GenericStruct<GenericStruct<int[]>> gs);
+    static bool DelegateTargetForNestedFunc<T>(T arg) => true;
+
+    public struct SimpleStruct
+    {
+        public DateTime dt;
+        public GenericStruct<DateTime> gs;
+    }
+
+    public struct GenericStruct<T>
+    {
+        public System.Collections.Generic.List<T> List;
+        public string StringField;
+
+        public static GenericStruct<bool[]> DelegateTargetForSignatureTest(Math m, GenericStruct<GenericStruct<T[]>> gs) => new GenericStruct<bool[]>();
+    }
+
+}
+
+public class DebuggerTest
+{
+    public static void run_all()
+    {
+        locals();
+    }
+
+    public static int locals()
+    {
+        int l_int = 1;
+        char l_char = 'A';
+        long l_long = Int64.MaxValue;
+        ulong l_ulong = UInt64.MaxValue;
+        locals_inner();
+        return 0;
+    }
+
+    static void locals_inner() { }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-test.csproj b/src/mono/wasm/debugger/tests/debugger-test.csproj
new file mode 100644 (file)
index 0000000..310ec81
--- /dev/null
@@ -0,0 +1,49 @@
+<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildApp">
+  <PropertyGroup>
+    <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+    <TargetArchitecture>wasm</TargetArchitecture>
+    <TargetOS>Browser</TargetOS>
+    <MicrosoftNetCoreAppRuntimePackDir>$(ArtifactsBinDir)microsoft.netcore.app.runtime.browser-wasm\Release\runtimes\browser-wasm\</MicrosoftNetCoreAppRuntimePackDir>
+    <BuildDir>$(MSBuildThisFileDirectory)obj\$(Configuration)\wasm</BuildDir>
+    <AppDir>$(MSBuildThisFileDirectory)bin\$(Configuration)\publish</AppDir>
+    <RuntimeBuildConfig Condition="'$(RuntimeBuildConfig)' == ''">$(Configuration)</RuntimeBuildConfig>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <OutputType>Library</OutputType>
+    <NoWarn>219</NoWarn>
+    <DebugType>portable</DebugType>
+  </PropertyGroup>
+
+  <Target Name="RebuildWasmAppBuilder">
+    <ItemGroup>
+      <WasmAppBuildProject Include="$(RepoTasksDir)mobile.tasks\WasmAppBuilder\WasmAppBuilder.csproj" />
+    </ItemGroup>
+
+    <MSBuild Projects="@(WasmAppBuildProject)"
+             Properties="Configuration=$(Configuration);MSBuildRestoreSessionId=$([System.Guid]::NewGuid())"
+             Targets="Restore"/>
+
+    <MSBuild Projects="@(WasmAppBuildProject)"
+             Properties="Configuration=$(Configuration)"
+             Targets="Build;Publish"/>
+  </Target>
+
+  <UsingTask TaskName="WasmAppBuilder" 
+             AssemblyFile="$(ArtifactsBinDir)WasmAppBuilder\$(Configuration)\$(NetCoreAppCurrent)\publish\WasmAppBuilder.dll"/>
+
+  <Target Name="BuildApp" DependsOnTargets="RebuildWasmAppBuilder;Build">
+    <ItemGroup>
+      <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)native"/>
+      <AssemblySearchPaths Include="$(MicrosoftNetCoreAppRuntimePackDir)lib\$(NetCoreAppCurrent)"/>
+    </ItemGroup>
+    <WasmAppBuilder
+      AppDir="$(AppDir)"
+      MicrosoftNetCoreAppRuntimePackDir="$(MicrosoftNetCoreAppRuntimePackDir)"
+      MainAssembly="$(ArtifactsBinDir)debugger-test/wasm/Debug/debugger-test.dll"
+      MainJS="$(MonoProjectRoot)wasm\runtime-test.js"
+      DebugLevel="1"
+      AssemblySearchPaths="@(AssemblySearchPaths)"
+      ExtraAssemblies="$(ArtifactsBinDir)\System.Runtime.InteropServices.JavaScript\$(NetCoreAppCurrent)-Browser-$(RuntimeConfiguration)\System.Runtime.InteropServices.JavaScript.dll"/>
+    <Exec Command="chmod a+x $(AppDir)/run-v8.sh" />
+  </Target>
+
+</Project>
diff --git a/src/mono/wasm/debugger/tests/debugger-test2.cs b/src/mono/wasm/debugger/tests/debugger-test2.cs
new file mode 100644 (file)
index 0000000..26817a2
--- /dev/null
@@ -0,0 +1,51 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+public class Misc
+{ //Only append content to this class as the test suite depends on line info
+    public static int CreateObject(int foo, int bar)
+    {
+        var f = new Fancy()
+        {
+            Foo = foo,
+            Bar = bar,
+        };
+
+        Console.WriteLine($"{f.Foo} {f.Bar}");
+        return f.Foo + f.Bar;
+    }
+}
+
+public class Fancy
+{
+    public int Foo;
+    public int Bar { get; set; }
+    public static void Types()
+    {
+        double dPI = System.Math.PI;
+        float fPI = (float) System.Math.PI;
+
+        int iMax = int.MaxValue;
+        int iMin = int.MinValue;
+        uint uiMax = uint.MaxValue;
+        uint uiMin = uint.MinValue;
+
+        long l = uiMax * (long) 2;
+        long lMax = long.MaxValue; // cannot be represented as double
+        long lMin = long.MinValue; // cannot be represented as double
+
+        sbyte sbMax = sbyte.MaxValue;
+        sbyte sbMin = sbyte.MinValue;
+        byte bMax = byte.MaxValue;
+        byte bMin = byte.MinValue;
+
+        short sMax = short.MaxValue;
+        short sMin = short.MinValue;
+        ushort usMin = ushort.MinValue;
+        ushort usMax = ushort.MaxValue;
+
+        var d = usMin + usMax;
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs
new file mode 100644 (file)
index 0000000..3a4ceea
--- /dev/null
@@ -0,0 +1,264 @@
+// 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;
+namespace DebuggerTests
+{
+    public class ValueTypesTest
+    { //Only append content to this class as the test suite depends on line info
+
+        public static void MethodWithLocalStructs()
+        {
+            var ss_local = new SimpleStruct("set in MethodWithLocalStructs", 1, DateTimeKind.Utc);
+            var gs_local = new GenericStruct<ValueTypesTest> { StringField = "gs_local#GenericStruct<ValueTypesTest>#StringField" };
+
+            ValueTypesTest vt_local = new ValueTypesTest
+            {
+                StringField = "string#0",
+                SimpleStructField = new SimpleStruct("SimpleStructField#string#0", 5, DateTimeKind.Local),
+                SimpleStructProperty = new SimpleStruct("SimpleStructProperty#string#0", 2, DateTimeKind.Utc), DT = new DateTime(2020, 1, 2, 3, 4, 5), RGB = RGB.Blue
+            };
+            Console.WriteLine($"Using the struct: {ss_local.gs.StringField}, gs: {gs_local.StringField}, {vt_local.StringField}");
+        }
+
+        public static void TestStructsAsMethodArgs()
+        {
+            var ss_local = new SimpleStruct("ss_local#SimpleStruct#string#0", 5, DateTimeKind.Local);
+            var ss_ret = MethodWithStructArgs("TestStructsAsMethodArgs#label", ss_local, 3);
+            Console.WriteLine($"got back ss_local: {ss_local.gs.StringField}, ss_ret: {ss_ret.gs.StringField}");
+        }
+
+        static SimpleStruct MethodWithStructArgs(string label, SimpleStruct ss_arg, int x)
+        {
+            Console.WriteLine($"- ss_arg: {ss_arg.str_member}");
+            ss_arg.Kind = DateTimeKind.Utc;
+            ss_arg.str_member = $"ValueTypesTest#MethodWithStructArgs#updated#ss_arg#str_member";
+            ss_arg.gs.StringField = $"ValueTypesTest#MethodWithStructArgs#updated#gs#StringField#{x}";
+            return ss_arg;
+        }
+
+        public static async Task<bool> MethodWithLocalStructsStaticAsync()
+        {
+            var ss_local = new SimpleStruct("set in MethodWithLocalStructsStaticAsync", 1, DateTimeKind.Utc);
+            var gs_local = new GenericStruct<int>
+            {
+                StringField = "gs_local#GenericStruct<ValueTypesTest>#StringField",
+                List = new System.Collections.Generic.List<int> { 5, 3 },
+                Options = Options.Option2
+
+            };
+
+            var result = await ss_local.AsyncMethodWithStructArgs(gs_local);
+            Console.WriteLine($"Using the struct: {ss_local.gs.StringField}, result: {result}");
+
+            return result;
+        }
+
+        public string StringField;
+        public SimpleStruct SimpleStructProperty { get; set; }
+        public SimpleStruct SimpleStructField;
+
+        public struct SimpleStruct
+        {
+            public string str_member;
+            public DateTime dt;
+            public GenericStruct<DateTime> gs;
+            public DateTimeKind Kind;
+
+            public SimpleStruct(string str, int f, DateTimeKind kind)
+            {
+                str_member = $"{str}#SimpleStruct#str_member";
+                dt = new DateTime(2020 + f, 1 + f, 2 + f, 3 + f, 5 + f, 6 + f);
+                gs = new GenericStruct<DateTime>
+                {
+                    StringField = $"{str}#SimpleStruct#gs#StringField",
+                    List = new System.Collections.Generic.List<DateTime> { new DateTime(2010 + f, 2 + f, 3 + f, 10 + f, 2 + f, 3 + f) },
+                    Options = Options.Option1
+                };
+                Kind = kind;
+            }
+
+            public Task<bool> AsyncMethodWithStructArgs(GenericStruct<int> gs)
+            {
+                Console.WriteLine($"placeholder line for a breakpoint");
+                if (gs.List.Count > 0)
+                    return Task.FromResult(true);
+
+                return Task.FromResult(false);
+            }
+        }
+
+        public struct GenericStruct<T>
+        {
+            public System.Collections.Generic.List<T> List;
+            public string StringField;
+
+            public Options Options { get; set; }
+        }
+
+        public DateTime DT { get; set; }
+        public RGB RGB;
+
+        public static void MethodWithLocalsForToStringTest(bool call_other)
+        {
+            var dt0 = new DateTime(2020, 1, 2, 3, 4, 5);
+            var dt1 = new DateTime(2010, 5, 4, 3, 2, 1);
+            var ts = dt0 - dt1;
+            var dto = new DateTimeOffset(dt0, new TimeSpan(4, 5, 0));
+            decimal dec = 123987123;
+            var guid = new Guid("3d36e07e-ac90-48c6-b7ec-a481e289d014");
+
+            var dts = new DateTime[]
+            {
+                new DateTime(1983, 6, 7, 5, 6, 10),
+                new DateTime(1999, 10, 15, 1, 2, 3)
+            };
+
+            var obj = new ClassForToStringTests
+            {
+                DT = new DateTime(2004, 10, 15, 1, 2, 3),
+                DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0)),
+                TS = ts,
+                Dec = 1239871,
+                Guid = guid
+            };
+
+            var sst = new StructForToStringTests
+            {
+                DT = new DateTime(2004, 10, 15, 1, 2, 3),
+                DTO = new DateTimeOffset(dt0, new TimeSpan(3, 15, 0)),
+                TS = ts,
+                Dec = 1239871,
+                Guid = guid
+            };
+            Console.WriteLine($"MethodWithLocalsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+            if (call_other)
+                MethodWithArgumentsForToStringTest(call_other, dt0, dt1, ts, dto, dec, guid, dts, obj, sst);
+        }
+
+        static void MethodWithArgumentsForToStringTest(
+            bool call_other, // not really used, just to help with using common code in the tests
+            DateTime dt0, DateTime dt1, TimeSpan ts, DateTimeOffset dto, decimal dec,
+            Guid guid, DateTime[] dts, ClassForToStringTests obj, StructForToStringTests sst)
+        {
+            Console.WriteLine($"MethodWithArgumentsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+        }
+
+        public static async Task MethodWithLocalsForToStringTestAsync(bool call_other)
+        {
+            var dt0 = new DateTime(2020, 1, 2, 3, 4, 5);
+            var dt1 = new DateTime(2010, 5, 4, 3, 2, 1);
+            var ts = dt0 - dt1;
+            var dto = new DateTimeOffset(dt0, new TimeSpan(4, 5, 0));
+            decimal dec = 123987123;
+            var guid = new Guid("3d36e07e-ac90-48c6-b7ec-a481e289d014");
+
+            var dts = new DateTime[]
+            {
+                new DateTime(1983, 6, 7, 5, 6, 10),
+                new DateTime(1999, 10, 15, 1, 2, 3)
+            };
+
+            var obj = new ClassForToStringTests
+            {
+                DT = new DateTime(2004, 10, 15, 1, 2, 3),
+                DTO = new DateTimeOffset(dt0, new TimeSpan(2, 14, 0)),
+                TS = ts,
+                Dec = 1239871,
+                Guid = guid
+            };
+
+            var sst = new StructForToStringTests
+            {
+                DT = new DateTime(2004, 10, 15, 1, 2, 3),
+                DTO = new DateTimeOffset(dt0, new TimeSpan(3, 15, 0)),
+                TS = ts,
+                Dec = 1239871,
+                Guid = guid
+            };
+            Console.WriteLine($"MethodWithLocalsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+            if (call_other)
+                await MethodWithArgumentsForToStringTestAsync(call_other, dt0, dt1, ts, dto, dec, guid, dts, obj, sst);
+        }
+
+        static async Task MethodWithArgumentsForToStringTestAsync(
+            bool call_other, // not really used, just to help with using common code in the tests
+            DateTime dt0, DateTime dt1, TimeSpan ts, DateTimeOffset dto, decimal dec,
+            Guid guid, DateTime[] dts, ClassForToStringTests obj, StructForToStringTests sst)
+        {
+            Console.WriteLine($"MethodWithArgumentsForToStringTest: {dt0}, {dt1}, {ts}, {dec}, {guid}, {dts[0]}, {obj.DT}, {sst.DT}");
+        }
+
+        public static void MethodUpdatingValueTypeMembers()
+        {
+            var obj = new ClassForToStringTests
+            {
+                DT = new DateTime(1, 2, 3, 4, 5, 6)
+            };
+            var vt = new StructForToStringTests
+            {
+                DT = new DateTime(4, 5, 6, 7, 8, 9)
+            };
+            Console.WriteLine($"#1");
+            obj.DT = new DateTime(9, 8, 7, 6, 5, 4);
+            vt.DT = new DateTime(5, 1, 3, 7, 9, 10);
+            Console.WriteLine($"#2");
+        }
+
+        public static async Task MethodUpdatingValueTypeLocalsAsync()
+        {
+            var dt = new DateTime(1, 2, 3, 4, 5, 6);
+            Console.WriteLine($"#1");
+            dt = new DateTime(9, 8, 7, 6, 5, 4);
+            Console.WriteLine($"#2");
+        }
+
+        public static void MethodUpdatingVTArrayMembers()
+        {
+            var ssta = new []
+            {
+                new StructForToStringTests { DT = new DateTime(1, 2, 3, 4, 5, 6) }
+            };
+            Console.WriteLine($"#1");
+            ssta[0].DT = new DateTime(9, 8, 7, 6, 5, 4);
+            Console.WriteLine($"#2");
+        }
+    }
+
+    class ClassForToStringTests
+    {
+        public DateTime DT;
+        public DateTimeOffset DTO;
+        public TimeSpan TS;
+        public decimal Dec;
+        public Guid Guid;
+    }
+
+    struct StructForToStringTests
+    {
+        public DateTime DT;
+        public DateTimeOffset DTO;
+        public TimeSpan TS;
+        public decimal Dec;
+        public Guid Guid;
+    }
+
+    public enum RGB
+    {
+        Red,
+        Green,
+        Blue
+    }
+
+    [Flags]
+    public enum Options
+    {
+        None = 0,
+        Option1 = 1,
+        Option2 = 2,
+        Option3 = 4,
+
+        All = Option1 | Option3
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/dependency.cs b/src/mono/wasm/debugger/tests/dependency.cs
new file mode 100644 (file)
index 0000000..3581a5a
--- /dev/null
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Simple
+{
+    public class Complex
+    {
+        public int A { get; set; }
+        public string B { get; set; }
+        object c;
+
+        public Complex(int a, string b)
+        {
+            A = a;
+            B = b;
+            this.c = this;
+        }
+
+        public int DoStuff()
+        {
+            return DoOtherStuff();
+        }
+
+        public int DoOtherStuff()
+        {
+            return DoEvenMoreStuff() - 1;
+        }
+
+        public int DoEvenMoreStuff()
+        {
+            return 1 + BreakOnThisMethod();
+        }
+
+        public int BreakOnThisMethod()
+        {
+            var x = A + 10;
+            c = $"{x}_{B}";
+
+            return x;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/mono/wasm/debugger/tests/other.js b/src/mono/wasm/debugger/tests/other.js
new file mode 100644 (file)
index 0000000..7ba8e66
--- /dev/null
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+function big_array_js_test (len) {
+       var big = new Array(len);
+       for (let i=0; i < len; i ++) {
+               big[i]=i + 1000;
+       }
+       console.log('break here');
+};
+
+function object_js_test () {
+       var obj = {
+               a_obj: { aa: 5, ab: 'foo' },
+               b_arr: [ 10, 12 ]
+       };
+
+       return obj;
+};
+
+function getters_js_test () {
+       var ptd = {
+               get Int () { return 5; },
+               get String () { return "foobar"; },
+               get DT () { return "dt"; },
+               get IntArray () { return [1,2,3]; },
+               get DTArray () { return ["dt0", "dt1"]; },
+               DTAutoProperty: "dt",
+               StringField: "string field value"
+       };
+       console.log (`break here`);
+       return ptd;
+}
diff --git a/src/mono/wasm/debugger/tests/runtime-debugger.js b/src/mono/wasm/debugger/tests/runtime-debugger.js
new file mode 100644 (file)
index 0000000..2e341d0
--- /dev/null
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+var Module = { 
+       onRuntimeInitialized: function () {
+               config.loaded_cb = function () {
+                       App.init ();
+               };
+               MONO.mono_load_runtime_and_bcl_args (config)
+       },
+};
index 093b9fd..0692c65 100644 (file)
@@ -73,6 +73,12 @@ var MonoSupportLib = {
                        },
                },
 
+               mono_wasm_get_exception_object: function() {
+                       var exception_obj = MONO.active_exception;
+                       MONO.active_exception = null;
+                       return exception_obj ;
+               },
+
                mono_wasm_get_call_stack: function() {
                        if (!this.mono_wasm_current_bp_id)
                                this.mono_wasm_current_bp_id = Module.cwrap ("mono_wasm_current_bp_id", 'number');
@@ -114,8 +120,10 @@ var MonoSupportLib = {
                                        continue;
                                }
 
-                               if (i + 1 < var_list.length)
-                                       o.value = _fixup_value(var_list[i + 1].value);
+                               if (i + 1 < var_list.length) {
+                                       _fixup_value(var_list[i + 1].value);
+                                       o = Object.assign (o, var_list [i + 1]);
+                               }
 
                                out_list.push (o);
                                i += 2;
@@ -145,6 +153,25 @@ var MonoSupportLib = {
                        return final_var_list;
                },
 
+               // Given `dotnet:object:foo:bar`,
+               // returns [ 'dotnet', 'object', 'foo:bar']
+               _split_object_id: function (id, delimiter = ':', count = 3) {
+                       if (id === undefined || id == "")
+                               return [];
+
+                       if (delimiter === undefined) delimiter = ':';
+                       if (count === undefined) count = 3;
+
+                       var var_arr = id.split (delimiter);
+                       var result = var_arr.splice (0, count - 1);
+
+                       if (var_arr.length > 0)
+                               result.push (var_arr.join (delimiter));
+                       return result;
+               },
+
+               //
+               // @var_list: [ { index: <var_id>, name: <var_name> }, .. ]
                mono_wasm_get_variables: function(scope, var_list) {
                        if (!this.mono_wasm_get_var_info)
                                this.mono_wasm_get_var_info = Module.cwrap ("mono_wasm_get_var_info", null, [ 'number', 'number', 'number']);
@@ -153,26 +180,34 @@ var MonoSupportLib = {
                        var numBytes = var_list.length * Int32Array.BYTES_PER_ELEMENT;
                        var ptr = Module._malloc(numBytes);
                        var heapBytes = new Int32Array(Module.HEAP32.buffer, ptr, numBytes);
-                       for (let i=0; i<var_list.length; i++)
-                               heapBytes[i] = var_list[i];
+                       for (let i=0; i<var_list.length; i++) {
+                               heapBytes[i] = var_list[i].index;
+                       }
 
                        this._async_method_objectId = 0;
                        this.mono_wasm_get_var_info (scope, heapBytes.byteOffset, var_list.length);
                        Module._free(heapBytes.byteOffset);
                        var res = MONO._fixup_name_value_objects (this.var_info);
 
-                       //Async methods are special in the way that local variables can be lifted to generated class fields
-                       //value of "this" comes here either
                        for (let i in res) {
-                               var name = res [i].name;
-                               if (name != undefined && name.indexOf ('>') > 0)
-                                       res [i].name = name.substring (1, name.indexOf ('>'));
-                       }
+                               var res_name = res [i].name;
+
+                               var value = res[i].value;
+                               if (this._async_method_objectId != 0) {
+                                       //Async methods are special in the way that local variables can be lifted to generated class fields
+                                       //value of "this" comes here either
+                                       if (res_name !== undefined && res_name.indexOf ('>') > 0) {
+                                               // For async methods, we get the names too, so use that
+                                               // ALTHOUGH, the name wouldn't have `<>` for method args
+                                               res [i].name = res_name.substring (1, res_name.indexOf ('>'));
+                                       }
 
-                       if (this._async_method_objectId != 0) {
-                               for (let i in res) {
-                                       if (res [i].value.isValueType != undefined && res [i].value.isValueType)
-                                               res [i].value.objectId = `dotnet:valuetype:${this._async_method_objectId}:${res [i].fieldOffset}`;
+                                       if (value.isValueType)
+                                               value.objectId = `dotnet:valuetype:${this._async_method_objectId}:${res [i].fieldOffset}`;
+                               } else if (res_name === undefined && var_list [i] !== undefined) {
+                                       // For non-async methods, we just have the var id, but we have the name
+                                       // from the caller
+                                       res [i].name = var_list [i].name;
                                }
                        }
 
@@ -182,6 +217,7 @@ var MonoSupportLib = {
                        return res;
                },
 
+
                mono_wasm_get_object_properties: function(objId, expandValueTypes) {
                        if (!this.mono_wasm_get_object_properties_info)
                                this.mono_wasm_get_object_properties_info = Module.cwrap ("mono_wasm_get_object_properties", null, [ 'number', 'bool' ]);
@@ -191,8 +227,10 @@ var MonoSupportLib = {
 
                        var res = MONO._filter_automatic_properties (MONO._fixup_name_value_objects (this.var_info));
                        for (var i = 0; i < res.length; i++) {
-                               if (res [i].value.isValueType != undefined && res [i].value.isValueType)
-                                       res [i].value.objectId = `dotnet:valuetype:${objId}:${res [i].fieldOffset}`;
+                               var res_val = res [i].value;
+                               // we might not have a `.value`, like in case of getters which have a `.get` instead
+                               if (res_val !== undefined && res_val.isValueType != undefined && res_val.isValueType)
+                                       res_val.objectId = `dotnet:valuetype:${objId}:${res [i].fieldOffset}`;
                        }
 
                        this.var_info = [];
@@ -209,8 +247,14 @@ var MonoSupportLib = {
 
                        var res = MONO._fixup_name_value_objects (this.var_info);
                        for (var i = 0; i < res.length; i++) {
-                               if (res [i].value.isValueType != undefined && res [i].value.isValueType)
+                               var prop_value = res [i].value;
+                               if (prop_value.isValueType) {
                                        res [i].value.objectId = `dotnet:array:${objId}:${i}`;
+                               } else if (prop_value.objectId !== undefined && prop_value.objectId.startsWith("dotnet:pointer")) {
+                                       prop_value.objectId = this._get_updated_ptr_id (prop_value.objectId, {
+                                               varName: `[${i}]`
+                                       });
+                               }
                        }
 
                        this.var_info = [];
@@ -255,12 +299,26 @@ var MonoSupportLib = {
 
                        for (let i in var_list) {
                                var value = var_list [i].value;
-                               if (value == undefined || value.type != "object")
+                               if (value === undefined)
                                        continue;
 
-                               if (value.isValueType != true || value.expanded != true) // undefined would also give us false
+                               if (value.objectId !== undefined && value.objectId.startsWith ("dotnet:pointer:")) {
+                                       var ptr_args = this._get_ptr_args (value.objectId);
+                                       if (ptr_args.varName === undefined) {
+                                               // It might have been already set in some cases, like arrays
+                                               // where the name would be `0`, but we want `[0]` for pointers,
+                                               // so the deref would look like `*[0]`
+                                               value.objectId = this._get_updated_ptr_id (value.objectId, {
+                                                       varName: var_list [i].name
+                                               });
+                                       }
+                               }
+
+                               if (value.type != "object" || value.isValueType != true || value.expanded != true) // undefined would also give us false
                                        continue;
 
+                               // Generate objectId for expanded valuetypes
+
                                var objectId = value.objectId;
                                if (objectId == undefined)
                                        objectId = `dotnet:valuetype:${this._next_value_type_id ()}`;
@@ -309,6 +367,104 @@ var MonoSupportLib = {
                        }
                },
 
+               _get_cfo_res_details: function (objectId, args) {
+                       if (!(objectId in this._call_function_res_cache))
+                               throw new Error(`Could not find any object with id ${objectId}`);
+
+                       var real_obj = this._call_function_res_cache [objectId];
+
+                       var descriptors = Object.getOwnPropertyDescriptors (real_obj);
+                       if (args.accessorPropertiesOnly) {
+                               Object.keys (descriptors).forEach (k => {
+                                       if (descriptors [k].get === undefined)
+                                               Reflect.deleteProperty (descriptors, k);
+                               });
+                       }
+
+                       var res_details = [];
+                       Object.keys (descriptors).forEach (k => {
+                               var new_obj;
+                               var prop_desc = descriptors [k];
+                               if (typeof prop_desc.value == "object") {
+                                       // convert `{value: { type='object', ... }}`
+                                       // to      `{ name: 'foo', value: { type='object', ... }}
+                                       new_obj = Object.assign ({ name: k }, prop_desc);
+                               } else if (prop_desc.value !== undefined) {
+                                       // This is needed for values that were not added by us,
+                                       // thus are like { value: 5 }
+                                       // instead of    { value: { type = 'number', value: 5 }}
+                                       //
+                                       // This can happen, for eg., when `length` gets added for arrays
+                                       // or `__proto__`.
+                                       new_obj = {
+                                               name: k,
+                                               // merge/add `type` and `description` to `d.value`
+                                               value: Object.assign ({ type: (typeof prop_desc.value), description: '' + prop_desc.value },
+                                                                                               prop_desc)
+                                       };
+                               } else if (prop_desc.get !== undefined) {
+                                       // The real_obj has the actual getter. We are just returning a placeholder
+                                       // If the caller tries to run function on the cfo_res object,
+                                       // that accesses this property, then it would be run on `real_obj`,
+                                       // which *has* the original getter
+                                       new_obj = {
+                                               name: k,
+                                               get: {
+                                                       className: "Function",
+                                                       description: `get ${k} () {}`,
+                                                       type: "function"
+                                               }
+                                       };
+                               } else {
+                                       new_obj = { name: k, value: { type: "symbol", value: "<Unknown>", description: "<Unknown>"} };
+                               }
+
+                               res_details.push (new_obj);
+                       });
+
+                       return { __value_as_json_string__: JSON.stringify (res_details) };
+               },
+
+               _get_ptr_args: function (objectId) {
+                       var parts = this._split_object_id (objectId);
+                       if (parts.length != 3)
+                               throw new Error (`Bug: Unexpected objectId format for a pointer, expected 3 parts: ${objectId}`);
+                       return JSON.parse (parts [2]);
+               },
+
+               _get_updated_ptr_id: function (objectId, new_args) {
+                       var old_args = {};
+                       if (typeof (objectId) === 'string' && objectId.length)
+                               old_args = this._get_ptr_args (objectId);
+
+                       return `dotnet:pointer:${JSON.stringify ( Object.assign (old_args, new_args) )}`;
+               },
+
+               _get_deref_ptr_value: function (objectId) {
+                       if (!this.mono_wasm_get_deref_ptr_value_info)
+                               this.mono_wasm_get_deref_ptr_value_info = Module.cwrap("mono_wasm_get_deref_ptr_value", null, ['number', 'number']);
+
+                       var ptr_args = this._get_ptr_args (objectId);
+                       if (ptr_args.ptr_addr == 0 || ptr_args.klass_addr == 0)
+                               throw new Error (`Both ptr_addr and klass_addr need to be non-zero, to dereference a pointer. objectId: ${objectId}`);
+
+                       this.var_info = [];
+                       var value_addr = new DataView (Module.HEAPU8.buffer).getUint32 (ptr_args.ptr_addr, /* littleEndian */ true);
+                       this.mono_wasm_get_deref_ptr_value_info (value_addr, ptr_args.klass_addr);
+
+                       var res = MONO._fixup_name_value_objects(this.var_info);
+                       if (res.length > 0) {
+                               if (ptr_args.varName === undefined)
+                                       throw new Error (`Bug: no varName found for the pointer. objectId: ${objectId}`);
+
+                               res [0].name = `*${ptr_args.varName}`;
+                       }
+
+                       res = this._post_process_details (res);
+                       this.var_info = [];
+                       return res;
+               },
+
                mono_wasm_get_details: function (objectId, args) {
                        var parts = objectId.split(":");
                        if (parts[0] != "dotnet")
@@ -334,51 +490,11 @@ var MonoSupportLib = {
                                        var containerObjectId = parts[2];
                                        return this._get_details_for_value_type(objectId, () => this.mono_wasm_get_object_properties(containerObjectId, true));
 
-                               case "cfo_res": {
-                                       if (!(objectId in this._call_function_res_cache))
-                                               throw new Error(`Could not find any object with id ${objectId}`);
-
-                                       var real_obj = this._call_function_res_cache [objectId];
-                                       if (args.accessorPropertiesOnly) {
-                                               // var val_accessors = JSON.stringify ([
-                                               //  {
-                                               //      name: "__proto__",
-                                               //      get: { type: "function", className: "Function", description: "function get __proto__ () {}", objectId: "dotnet:cfo_res:9999" },
-                                               //      set: { type: "function", className: "Function", description: "function set __proto__ () {}", objectId: "dotnet:cfo_res:8888" },
-                                               //      isOwn: false
-                                               //  }], undefined, 4);
-                                               return { __value_as_json_string__:  "[]" };
-                                       }
-
-                                       // behaving as if (args.ownProperties == true)
-                                       var descriptors = Object.getOwnPropertyDescriptors (real_obj);
-                                       var own_properties = [];
-                                       Object.keys (descriptors).forEach (k => {
-                                               var new_obj;
-                                               var prop_desc = descriptors [k];
-                                               if (typeof prop_desc.value == "object") {
-                                                       // convert `{value: { type='object', ... }}`
-                                                       // to      `{ name: 'foo', value: { type='object', ... }}
-                                                       new_obj = Object.assign ({ name: k}, prop_desc);
-                                               } else {
-                                                       // This is needed for values that were not added by us,
-                                                       // thus are like { value: 5 }
-                                                       // instead of    { value: { type = 'number', value: 5 }}
-                                                       //
-                                                       // This can happen, for eg., when `length` gets added for arrays
-                                                       // or `__proto__`.
-                                                       new_obj = {
-                                                               name: k,
-                                                               // merge/add `type` and `description` to `d.value`
-                                                               value: Object.assign ({ type: (typeof prop_desc.value), description: '' + prop_desc.value },
-                                                                                                               prop_desc)
-                                                       };
-                                               }
+                               case "cfo_res":
+                                       return this._get_cfo_res_details (objectId, args);
 
-                                               own_properties.push (new_obj);
-                                       });
-
-                                       return { __value_as_json_string__: JSON.stringify (own_properties) };
+                               case "pointer": {
+                                       return this._get_deref_ptr_value (objectId);
                                }
 
                                default:
@@ -397,39 +513,83 @@ var MonoSupportLib = {
                                delete this._cache_call_function_res[objectId];
                },
 
+               _invoke_getter_on_object: function (objectId, name) {
+                       if (!this.mono_wasm_invoke_getter_on_object)
+                               this.mono_wasm_invoke_getter_on_object = Module.cwrap ("mono_wasm_invoke_getter_on_object", 'void', [ 'number', 'string' ]);
+
+                       if (objectId < 0) {
+                               // invalid id
+                               return [];
+                       }
+
+                       this.mono_wasm_invoke_getter_on_object (objectId, name);
+                       var getter_res = MONO._post_process_details (MONO.var_info);
+
+                       MONO.var_info = [];
+                       return getter_res [0];
+               },
+
+               _create_proxy_from_object_id: function (objectId) {
+                       var details = this.mono_wasm_get_details(objectId);
+
+                       if (this._is_object_id_array (objectId))
+                               return details.map (p => p.value);
+
+                       var objIdParts = objectId.split (':');
+                       var objIdNum = -1;
+                       if (objectId.startsWith ("dotnet:object:"))
+                               objIdNum = objIdParts [2];
+
+                       var proxy = {};
+                       Object.keys (details).forEach (p => {
+                               var prop = details [p];
+                               if (prop.get !== undefined) {
+                                       // TODO: `set`
+
+                                       // We don't add a `get` for non-object types right now,
+                                       // so, we shouldn't get here with objIdNum==-1
+                                       Object.defineProperty (proxy,
+                                                       prop.name,
+                                                       { get () { return MONO._invoke_getter_on_object (objIdNum, prop.name); } }
+                                       );
+                               } else {
+                                       proxy [prop.name] = prop.value;
+                               }
+                       });
+
+                       return proxy;
+               },
+
                mono_wasm_call_function_on: function (request) {
+                       if (request.arguments != undefined && !Array.isArray (request.arguments))
+                               throw new Error (`"arguments" should be an array, but was ${request.arguments}`);
+
                        var objId = request.objectId;
                        var proxy;
 
                        if (objId in this._call_function_res_cache) {
                                proxy = this._call_function_res_cache [objId];
                        } else if (!objId.startsWith ('dotnet:cfo_res:')) {
-                               var details = this.mono_wasm_get_details(objId);
-                               var target_is_array = this._is_object_id_array (objId);
-                               proxy = target_is_array ? [] : {};
-
-                               Object.keys(details).forEach(p => {
-                                       var prop = details[p];
-                                       if (target_is_array) {
-                                               proxy.push(prop.value);
-                                       } else {
-                                               if (prop.name != undefined)
-                                                       proxy [prop.name] = prop.value;
-                                               else // when can this happen??
-                                                       proxy[''+p] = prop.value;
-                                       }
-                               });
+                               proxy = this._create_proxy_from_object_id (objId);
                        }
 
-                       var fn_args = request.arguments != undefined ? request.arguments.map(a => a.value) : [];
+                       var fn_args = request.arguments != undefined ? request.arguments.map(a => JSON.stringify(a.value)) : [];
                        var fn_eval_str = `var fn = ${request.functionDeclaration}; fn.call (proxy, ...[${fn_args}]);`;
 
                        var fn_res = eval (fn_eval_str);
-                       if (request.returnByValue)
+                       if (fn_res == undefined) // should we just return undefined?
+                               throw Error ('Function returned undefined result');
+
+                       // primitive type
+                       if (Object (fn_res) !== fn_res)
                                return fn_res;
 
-                       if (fn_res == undefined)
-                               throw Error ('Function returned undefined result');
+                       // return .value, if it is a primitive type
+                       if (fn_res.value !== undefined && Object (fn_res.value.value) !== fn_res.value.value)
+                               return fn_res.value;
+
+                       if (request.returnByValue)
+                               return {type: "object", value: fn_res};
 
                        var fn_res_id = this._cache_call_function_res (fn_res);
                        if (Object.getPrototypeOf (fn_res) == Array.prototype) {
@@ -445,24 +605,46 @@ var MonoSupportLib = {
                        }
                },
 
+               _clear_per_step_state: function () {
+                       this._next_value_type_id_var = 0;
+                       this._value_types_cache = {};
+               },
+
+               mono_wasm_debugger_resume: function () {
+                       this._clear_per_step_state ();
+               },
+               
                mono_wasm_start_single_stepping: function (kind) {
                        console.log (">> mono_wasm_start_single_stepping " + kind);
                        if (!this.mono_wasm_setup_single_step)
                                this.mono_wasm_setup_single_step = Module.cwrap ("mono_wasm_setup_single_step", 'number', [ 'number']);
 
-                       this._next_value_type_id_var = 0;
-                       this._value_types_cache = {};
+                       this._clear_per_step_state ();
 
                        return this.mono_wasm_setup_single_step (kind);
                },
 
+               mono_wasm_set_pause_on_exceptions: function (state) {
+                       if (!this.mono_wasm_pause_on_exceptions)
+                               this.mono_wasm_pause_on_exceptions = Module.cwrap ("mono_wasm_pause_on_exceptions", 'number', [ 'number']);
+                       var state_enum = 0;
+                       switch (state) {
+                               case 'uncaught':
+                                       state_enum = 1; //EXCEPTION_MODE_UNCAUGHT
+                                       break;
+                               case 'all':
+                                       state_enum = 2; //EXCEPTION_MODE_ALL
+                                       break;
+                       }
+                       return this.mono_wasm_pause_on_exceptions (state_enum);
+               },
+
                mono_wasm_runtime_ready: function () {
                        this.mono_wasm_runtime_is_ready = true;
                        // DO NOT REMOVE - magic debugger init function
                        console.debug ("mono_wasm_runtime_ready", "fe00e07a-5519-4dfe-b35a-f867dbaf2e28");
 
-                       this._next_value_type_id_var = 0;
-                       this._value_types_cache = {};
+                       this._clear_per_step_state ();
 
                        // FIXME: where should this go?
                        this._next_call_function_res_id = 0;
@@ -484,7 +666,7 @@ var MonoSupportLib = {
                },
 
                // Set environment variable NAME to VALUE
-               // Should be called before mono_load_runtime_and_bcl () in most cases
+               // Should be called before mono_load_runtime_and_bcl () in most cases 
                mono_wasm_setenv: function (name, value) {
                        if (!this.wasm_setenv)
                                this.wasm_setenv = Module.cwrap ('mono_wasm_setenv', null, ['string', 'string']);
@@ -663,12 +845,12 @@ var MonoSupportLib = {
 
                // deprecated
                mono_load_runtime_and_bcl: function (
-                       unused_vfs_prefix, deploy_prefix, enable_debugging, file_list, loaded_cb, fetch_file_cb
+                       unused_vfs_prefix, deploy_prefix, debug_level, file_list, loaded_cb, fetch_file_cb
                ) {
                        var args = {
                                fetch_file_cb: fetch_file_cb,
                                loaded_cb: loaded_cb,
-                               enable_debugging: enable_debugging,
+                               debug_level: debug_level,
                                assembly_root: deploy_prefix,
                                assets: []
                        };
@@ -693,7 +875,7 @@ var MonoSupportLib = {
                // Initializes the runtime and loads assemblies, debug information, and other files.
                // @args is a dictionary-style Object with the following properties:
                //    assembly_root: (required) the subfolder containing managed assemblies and pdbs
-               //    enable_debugging: (required)
+               //    debug_level or enable_debugging: (required)
                //    assets: (required) a list of assets to load along with the runtime. each asset
                //     is a dictionary-style Object with the following properties:
                //        name: (required) the name of the asset, including extension.
@@ -777,7 +959,7 @@ var MonoSupportLib = {
 
                        if (ENVIRONMENT_IS_SHELL || ENVIRONMENT_IS_NODE) {
                                try {
-                                       load_runtime ("unused", args.enable_debugging);
+                                       load_runtime ("unused", args.debug_level);
                                } catch (ex) {
                                        print ("MONO_WASM: load_runtime () failed: " + ex);
                                        print ("MONO_WASM: Stacktrace: \n");
@@ -787,7 +969,7 @@ var MonoSupportLib = {
                                        wasm_exit (1);
                                }
                        } else {
-                               load_runtime ("unused", args.enable_debugging);
+                               load_runtime ("unused", args.debug_level);
                        }
 
                        MONO.mono_wasm_runtime_ready ();
@@ -795,6 +977,8 @@ var MonoSupportLib = {
                },
 
                _load_assets_and_runtime: function (args) {
+                       if (args.enable_debugging)
+                               args.debug_level = args.enable_debugging;
                        if (args.assembly_list)
                                throw new Error ("Invalid args (assembly_list was replaced by assets)");
                        if (args.runtime_assets)
@@ -999,16 +1183,31 @@ var MonoSupportLib = {
                        });
                },
 
-               _mono_wasm_add_getter_var: function(className) {
+               _mono_wasm_add_getter_var: function(className, invokable) {
                        fixed_class_name = MONO._mono_csharp_fixup_class_name (className);
-                       var value = `${fixed_class_name} { get; }`;
-                       MONO.var_info.push({
-                               value: {
-                                       type: "symbol",
-                                       value: value,
-                                       description: value
-                               }
-                       });
+                       if (invokable != 0) {
+                               var name;
+                               if (MONO.var_info.length > 0)
+                                       name = MONO.var_info [MONO.var_info.length - 1].name;
+                               name = (name === undefined) ? "" : name;
+
+                               MONO.var_info.push({
+                                       get: {
+                                               className: "Function",
+                                               description: `get ${name} () {}`,
+                                               type: "function",
+                                       }
+                               });
+                       } else {
+                               var value = `${fixed_class_name} { get; }`;
+                               MONO.var_info.push({
+                                       value: {
+                                               type: "symbol",
+                                               description: value,
+                                               value: value,
+                                       }
+                               });
+                       }
                },
 
                _mono_wasm_add_array_var: function(className, objectId, length) {
@@ -1069,7 +1268,7 @@ var MonoSupportLib = {
                                break;
 
                        case "getter":
-                               MONO._mono_wasm_add_getter_var (str_value);
+                               MONO._mono_wasm_add_getter_var (str_value, value);
                                break;
 
                        case "array":
@@ -1077,13 +1276,26 @@ var MonoSupportLib = {
                                break;
 
                        case "pointer": {
-                               MONO.var_info.push ({
-                                       value: {
-                                               type: "symbol",
-                                               value: str_value,
-                                               description: str_value
-                                       }
-                               });
+                               var fixed_value_str = MONO._mono_csharp_fixup_class_name (str_value);
+                               if (value.klass_addr == 0 || value.ptr_addr == 0 || fixed_value_str.startsWith ('(void*')) {
+                                       // null or void*, which we can't deref
+                                       MONO.var_info.push({
+                                               value: {
+                                                       type: "symbol",
+                                                       value: fixed_value_str,
+                                                       description: fixed_value_str
+                                               }
+                                       });
+                               } else {
+                                       MONO.var_info.push({
+                                               value: {
+                                                       type: "object",
+                                                       className: fixed_value_str,
+                                                       description: fixed_value_str,
+                                                       objectId: this._get_updated_ptr_id ('', value)
+                                               }
+                                       });
+                               }
                                }
                                break;
 
index a9a6ffc..4ca72c5 100644 (file)
@@ -29,6 +29,7 @@ public class WasmAppBuilder : Task
     public string? MainJS { get; set; }
     [Required]
     public ITaskItem[]? AssemblySearchPaths { get; set; }
+    public int DebugLevel { get; set; }
     public ITaskItem[]? ExtraAssemblies { get; set; }
     public ITaskItem[]? FilesToIncludeInFileSystem { get; set; }
     public ITaskItem[]? RemoteSources { get; set; }
@@ -41,8 +42,8 @@ public class WasmAppBuilder : Task
     {
         [JsonPropertyName("assembly_root")]
         public string AssemblyRoot { get; set; } = "managed";
-        [JsonPropertyName("enable_debugging")]
-        public int EnableDebugging { get; set; } = 0;
+        [JsonPropertyName("debug_level")]
+        public int DebugLevel { get; set; } = 0;
         [JsonPropertyName("assets")]
         public List<object> Assets { get; } = new List<object>();
         [JsonPropertyName("remote_sources")]
@@ -116,8 +117,15 @@ public class WasmAppBuilder : Task
         // Create app
         Directory.CreateDirectory(AppDir!);
         Directory.CreateDirectory(Path.Join(AppDir, config.AssemblyRoot));
-        foreach (var assembly in _assemblies!.Values)
+        foreach (var assembly in _assemblies!.Values) {
             File.Copy(assembly.Location, Path.Join(AppDir, config.AssemblyRoot, Path.GetFileName(assembly.Location)), true);
+            if (DebugLevel > 0) {
+                var pdb = assembly.Location;
+                pdb = Path.ChangeExtension(pdb, ".pdb");
+                if (File.Exists(pdb))
+                    File.Copy(pdb, Path.Join(AppDir, config.AssemblyRoot, Path.GetFileName(pdb)), true);
+            }
+        }
 
         List<string> nativeAssets = new List<string>() { "dotnet.wasm", "dotnet.js", "dotnet.timezones.blat" };
 
@@ -130,8 +138,17 @@ public class WasmAppBuilder : Task
             File.Copy(Path.Join (MicrosoftNetCoreAppRuntimePackDir, "native", f), Path.Join(AppDir, f), true);
         File.Copy(MainJS!, Path.Join(AppDir, "runtime.js"),  true);
 
-        foreach (var assembly in _assemblies.Values)
+        foreach (var assembly in _assemblies.Values) {
             config.Assets.Add(new AssemblyEntry (Path.GetFileName(assembly.Location)));
+            if (DebugLevel > 0) {
+                var pdb = assembly.Location;
+                pdb = Path.ChangeExtension(pdb, ".pdb");
+                if (File.Exists(pdb))
+                    config.Assets.Add(new AssemblyEntry (Path.GetFileName(pdb)));
+            }
+        }
+
+        config.DebugLevel = DebugLevel;
 
         if (FilesToIncludeInFileSystem != null)
         {