From: Thays Grazia Date: Wed, 5 Aug 2020 22:34:41 +0000 (-0300) Subject: [wasm] Moving wasm debugger to dotnet/runtime (#40146) X-Git-Tag: submit/tizen/20210909.063632~6197 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=03d2e48212d57f6ca9e44a127de965e528a1bf85;p=platform%2Fupstream%2Fdotnet%2Fruntime.git [wasm] Moving wasm debugger to dotnet/runtime (#40146) * Moving Wasm debugger to dotnet/runtime The build is working. The tests are running. - make -C src/mono/wasm/ run-debugger-tests --- diff --git a/src/mono/wasm/Makefile b/src/mono/wasm/Makefile index 978bd1c..398bd0e 100644 --- a/src/mono/wasm/Makefile +++ b/src/mono/wasm/Makefile @@ -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 index 0000000..9a038a0 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugHost/BrowserDebugHost.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.0 + + + + + + + diff --git a/src/mono/wasm/debugger/BrowserDebugHost/Program.cs b/src/mono/wasm/debugger/BrowserDebugHost/Program.cs new file mode 100644 index 0000000..c45b9ce --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugHost/Program.cs @@ -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() + .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(ctx.Configuration); + services.Configure(options => + { + options.ChromePath = options.ChromePath ?? chromePath; + options.AppPath = appPath; + options.PagePath = pagePath; + options.DevToolsUrl = new Uri("http://localhost:0"); + }); + }) + .UseStartup() + .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 index 0000000..16292f9 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugHost/Startup.cs @@ -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(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 optionsAccessor, IWebHostEnvironment env) + { + var options = optionsAccessor.CurrentValue; + app.UseDeveloperExceptionPage() + .UseWebSockets() + .UseDebugProxy(options); + } + } + + static class DebugExtensions + { + public static Dictionary MapValues(Dictionary response, HttpContext context, Uri debuggerHost) + { + var filtered = new Dictionary(); + 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, HttpContext, Uri, Dictionary> 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>(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[]>(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 ProxyGetJsonAsync(string url) + { + using(var httpClient = new HttpClient()) + { + var response = await httpClient.GetAsync(url); + return await JsonSerializer.DeserializeAsync(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 index 0000000..953eb86 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugHost/TestHarnessStartup.cs @@ -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(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> extract_conn_url) + { + + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = 400; + return; + } + + var tcs = new TaskCompletionSource(); + + 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 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(); + 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 index 0000000..5d9d173 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/AssemblyInfo.cs @@ -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 index 0000000..4063416 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.1 + true + + + + + + + + + + diff --git a/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.sln b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.sln new file mode 100644 index 0000000..cfb208d --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/BrowserDebugProxy.sln @@ -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 index 0000000..aedcfb2 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebugStore.cs @@ -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 Locations { get; } = new List(); + + public override string ToString() => $"BreakpointRequest Assembly: {Assembly} File: {File} Line: {Line} Column: {Column}"; + + public object AsSetBreakpointByUrlResponse(IEnumerable 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(); + if (url == null) + { + var urlRegex = request?["urlRegex"].Value(); + 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(); + var column = request?["columnNumber"]?.Value(); + + 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(), out var id)) + return null; + + var line = obj["lineNumber"]?.Value(); + var column = obj["columnNumber"]?.Value(); + if (id == null || line == null || column == null) + return null; + + return new SourceLocation(id, line.Value, column.Value); + } + + internal class LocationComparer : EqualityComparer + { + 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(); + + 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 methods; + + public TypeInfo(AssemblyInfo assembly, TypeDefinition type) + { + this.assembly = assembly; + this.type = type; + methods = new List(); + } + + public string Name => type.Name; + public string FullName => type.FullName; + public List Methods => methods; + + public override string ToString() => "TypeInfo('" + FullName + "')"; + } + + class AssemblyInfo + { + static int next_id; + ModuleDefinition image; + readonly int id; + readonly ILogger logger; + Dictionary methods = new Dictionary(); + Dictionary sourceLinkMappings = new Dictionary(); + Dictionary typesByName = new Dictionary(); + readonly List sources = new List(); + 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(); + + 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>(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 Sources => this.sources; + + public Dictionary 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 methods; + AssemblyInfo assembly; + int id; + Document doc; + + internal SourceFile(AssemblyInfo assembly, int id, Document doc, Uri sourceLinkUri) + { + this.methods = new Dictionary(); + 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 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 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(); + } + + public async Task 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 assemblies = new List(); + 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 Data { get; set; } + } + + public async IAsyncEnumerable 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(); + var pdb_files = new List(); + 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 steps = new List(); + 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(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 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 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(); + 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 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 index 0000000..3594111 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DebuggerProxy.cs @@ -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 index 0000000..45edb96 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsHelper.cs @@ -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(), 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(), "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 BreakpointRequests { get; } = new Dictionary(); + + public TaskCompletionSource ready = null; + public bool IsRuntimeReady => ready != null && ready.Task.IsCompleted; + + public int Id { get; set; } + public object AuxData { get; set; } + + public List CallStack { get; set; } + + public string[] LoadedFiles { get; set; } + internal DebugStore store; + public TaskCompletionSource Source { get; } = new TaskCompletionSource(); + + public Dictionary LocalsCache = new Dictionary(); + + 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 index 0000000..656bd91 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/DevToolsProxy.cs @@ -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 pending; + + public WebSocket Ws { get; private set; } + public Task CurrentSend { get { return current_send; } } + public DevToolsQueue(WebSocket sock) + { + this.Ws = sock; + pending = new List(); + } + + 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(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(pending[0]), WebSocketMessageType.Text, true, token); + return current_send; + } + return null; + } + } + + internal class DevToolsProxy + { + TaskCompletionSource side_exception = new TaskCompletionSource(); + TaskCompletionSource client_initiated_close = new TaskCompletionSource(); + Dictionary> pending_cmds = new Dictionary>(); + ClientWebSocket browser; + WebSocket ide; + int next_cmd_id; + List pending_ops = new List(); + List queues = new List(); + + protected readonly ILogger logger; + + public DevToolsProxy(ILoggerFactory loggerFactory) + { + logger = loggerFactory.CreateLogger(); + } + + protected virtual Task AcceptEvent(SessionId sessionId, string method, JObject args, CancellationToken token) + { + return Task.FromResult(false); + } + + protected virtual Task AcceptCommand(MessageId id, string method, JObject args, CancellationToken token) + { + return Task.FromResult(false); + } + + async Task 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(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()), res["method"].Value(), res["params"] as JObject, token)); + else + OnResponse(new MessageId(res["sessionId"]?.Value(), res["id"].Value()), 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(), res["id"].Value()), + res["method"].Value(), + res["params"] as JObject, token)); + } + } + + internal async Task SendCommand(SessionId id, string method, JObject args, CancellationToken token) + { + //Log ("verbose", $"sending command {method}: {args}"); + return await SendCommandInternal(id, method, args, token); + } + + Task 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(); + + 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) 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) 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) 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) 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 index 0000000..982fa87 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -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 thisExpressions = new List(); + 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 variables = new List(); + public List thisList = new List(); + public List methodCall = new List(); + public List values = new List(); + + 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 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(); + var subType = variable["subtype"]?.Value(); + + switch (type) + { + case "string": + return value?.Value(); + case "number": + return value?.Value(); + case "boolean": + return value?.Value(); + 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(); + object value = ConvertJSToCSharpType(variable); + + switch (type) + { + case "object": + { + if (subType == "null") + return variable["className"].Value(); + 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 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 index 0000000..197d499 --- /dev/null +++ b/src/mono/wasm/debugger/BrowserDebugProxy/MonoProxy.cs @@ -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 sessions = new HashSet(); + Dictionary contexts = new Dictionary(); + + 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 SendMonoCommand(SessionId id, MonoCommands cmd, CancellationToken token) => SendCommand(id, "Runtime.evaluate", JObject.FromObject(cmd), token); + + protected override async Task 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(); + } + 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(); + if (aux_data != null) + { + var is_default = aux_data["isDefault"]?.Value(); + 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(); + + 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() ?? ""; + + 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 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() ?? false; + } + + static int bpIdGenerator; + + protected override async Task 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(); + return await OnGetScriptSource(id, script, token); + } + + case "Runtime.compileScript": + { + var exp = args?["expression"]?.Value(); + 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(); + 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(), 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 typeName = args["typeName"]?.Value(); + string methodName = args["methodName"]?.Value(); + 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}/::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 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(); + 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 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(); + 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(); + 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(); + foreach (var frame in orig_callframes) + { + var function_name = frame["functionName"]?.Value(); + var url = frame["url"]?.Value(); + if ("mono_wasm_fire_bp" == function_name ||"_mono_wasm_fire_bp" == function_name) + { + var frames = new List(); + int frame_id = 0; + var the_mono_frames = res.Value?["result"] ? ["value"] ? ["frames"]?.Values(); + + foreach (var mono_frame in the_mono_frames) + { + ++frame_id; + var il_pos = mono_frame["il_pos"].Value(); + var method_token = mono_frame["method_token"].Value(); + var assembly_name = mono_frame["assembly_name"].Value(); + + // This can be different than `method.Name`, like in case of generic methods + var method_name = mono_frame["method_name"]?.Value(); + + 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 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(); + + 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 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()?.ToArray(); + thisValue = scope_values?.FirstOrDefault(v => v["name"]?.Value() == "this"); + + if (!only_search_on_this) + { + if (thisValue != null && expression == "this") + return thisValue; + + var value = scope_values.SingleOrDefault(sv => sv["name"]?.Value() == 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().ToArray(); + var foundValue = scope_values.FirstOrDefault(v => v["name"].Value() == expression); + if (foundValue != null) + { + foundValue["fromThis"] = true; + context.LocalsCache[foundValue["name"].Value()] = foundValue; + return foundValue; + } + } + return null; + } + + async Task 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 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().ToArray(); + + if (values == null || values.Length == 0) + return Result.OkFromObject(new { result = Array.Empty() }); + + foreach (var value in values) + ctx.LocalsCache[value["name"]?.Value()] = value; + + return Result.OkFromObject(new { result = values }); + } + catch (Exception exception) + { + Log("verbose", $"Error resolving scope properties {exception.Message}"); + return Result.Exception(exception); + } + } + + async Task 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(); + + 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 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(); + } + + 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 RuntimeReady(SessionId sessionId, CancellationToken token) + { + var context = GetContext(sessionId); + if (Interlocked.CompareExchange(ref context.ready, new TaskCompletionSource(), 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(); + + 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(); + + 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(); + + 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 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 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 index 0000000..022e41c --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/ArrayTests.cs @@ -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", + local_var_name_prefix: "gclass", + array : new [] + { + TObject("DebuggerTests.GenericClass", is_null : true), + TObject("DebuggerTests.GenericClass"), + TObject("DebuggerTests.GenericClass") + }, + 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", + local_var_name_prefix: "gvclass", + array : new [] + { + TValueType("DebuggerTests.SimpleGenericStruct"), + TValueType("DebuggerTests.SimpleGenericStruct") + }, + 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", + local_var_name_prefix: "gvclass", + array : new [] + { + TValueType("DebuggerTests.SimpleGenericStruct"), + TValueType("DebuggerTests.SimpleGenericStruct") + }, + 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()); + 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()); + var l_obj = GetAndAssertObjectWithName(locals, name); + var l_objectId = l_obj["value"]["objectId"]?.Value(); + + 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() == 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 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(), 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()); + 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()); + await CheckProps(frame_locals, new + { + call_other = TBool(false), + gvclass_arr = TArray("DebuggerTests.SimpleGenericStruct[]", 2), + gvclass_arr_empty = TArray("DebuggerTests.SimpleGenericStruct[]"), + gvclass_arr_null = TObject("DebuggerTests.SimpleGenericStruct[]", is_null : true), + gvclass = TValueType("DebuggerTests.SimpleGenericStruct"), + // 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()); + await CheckProps(frame_locals, new + { + t1 = TObject("DebuggerTests.SimpleGenericStruct"), + @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()); + 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()); + await CheckProps(frame_locals, new + { + sc_arg = TObject("DebuggerTests.SimpleClass"), + @this = TValueType("DebuggerTests.Point"), + local_gs = TValueType("DebuggerTests.SimpleGenericStruct") + }, + "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 index 0000000..5c66bb2 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/CallFunctionOnTests.cs @@ -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>>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(), + 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(), + 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(), $"{label}-type"); + AssertEqual(className, actual["className"]?.Value(), $"{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 + { + + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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()); + 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") + }, "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", 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(), + 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(), + 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") + }, $"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", 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(), + 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(), + 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(), "cfo-res-type"); + + AssertEqual(JTokenType.Array, result.Value["result"]["value"].Type, "cfo-res-value-jsontype"); + var actual = result.Value["result"] ? ["value"].Values().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(), $"[{i}]"); + else + { + AssertEqual("number", actual[i] ? ["type"]?.Value(), $"[{i}]-type"); + AssertEqual(exp_num.ToString(), actual[i] ? ["description"]?.Value(), $"[{i}]-description"); + AssertEqual(exp_num, actual[i] ? ["value"]?.Value(), $"[{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(), "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()); + var obj = GetAndAssertObjectWithName(frame_locals, "big"); + var obj_id = obj["value"]["objectId"].Value(); + + 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 SilentErrorsTestData(bool? silent) => new TheoryData + { { "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()); + var obj = GetAndAssertObjectWithName(frame_locals, "big"); + var big_obj_id = obj["value"]["objectId"].Value(); + 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()?.Contains(error_msg); + Assert.True((hasErrorMessage ?? false), "Exception message not found"); + }); + } + + public static TheoryData, bool> GettersTestData(bool use_cfo) => new TheoryData, 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 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 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 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + 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(); + 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() == "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() == "StringField"), "StringField should be returned for `accessorPropertiesOnly=false`"); + }); + + async Task 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 InvokeGetter(JToken obj, string fn, object arguments) => await ctx.cli.SendCommand( + "Runtime.callFunctionOn", + JObject.FromObject(new + { + functionDeclaration = fn, + objectId = obj["value"] ? ["objectId"]?.Value(), + 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 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()); + var obj = GetAndAssertObjectWithName(frame_locals, local_name); + var obj_id = obj["value"]["objectId"].Value(); + + 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() + }); + + 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 index 0000000..6b7a560 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DateTimeTests.cs @@ -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 index 0000000..09b79eb --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DebuggerTestSuite.csproj @@ -0,0 +1,20 @@ + + + + $(NetCoreAppCurrent) + true + + + + + + + + + + + + + + + diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs b/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs new file mode 100644 index 0000000..e186ddc --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DelegateTests.cs @@ -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()); + + await CheckProps(locals, new + { + fn_func = TDelegate("System.Func", "bool |(Math)"), + fn_func_null = TObject("System.Func", is_null : true), + fn_func_arr = TArray("System.Func[]", 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", "bool |(Math)"), + fn_func_null_unused = TObject("System.Func", is_null : true), + fn_func_arr_unused = TArray("System.Func[]", 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", + "bool |(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", + "bool |(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()); + + await CheckProps(locals, new + { + fn_func = TDelegate("System.Func>, Math.GenericStruct>", + "Math.GenericStruct |(Math,Math.GenericStruct>)"), + + fn_func_del = TDelegate("System.Func>, Math.GenericStruct>", + "Math.GenericStruct DelegateTargetForSignatureTest (Math,Math.GenericStruct>)"), + + fn_func_null = TObject("System.Func>, Math.GenericStruct>", is_null : true), + fn_func_only_ret = TDelegate("System.Func", "bool |()"), + fn_func_arr = TArray("System.Func>, Math.GenericStruct>[]", 1), + + fn_del = TDelegate("Math.DelegateForSignatureTest", + "Math.GenericStruct DelegateTargetForSignatureTest (Math,Math.GenericStruct>)"), + + fn_del_l = TDelegate("Math.DelegateForSignatureTest", + "Math.GenericStruct |(Math,Math.GenericStruct>)"), + + 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>"), + fn_void_del = TDelegate("Math.DelegateWithVoidReturn", + "void DelegateTargetWithVoidReturn (Math.GenericStruct)"), + + fn_void_del_arr = TArray("Math.DelegateWithVoidReturn[]", 1), + fn_void_del_null = TObject("Math.DelegateWithVoidReturn", is_null : true), + gs = TValueType("Math.GenericStruct"), + rets = TArray("Math.GenericStruct[]", 6) + }, "locals"); + + await CompareObjectPropertiesFor(locals, "fn_func_arr", new [] + { + TDelegate( + "System.Func>, Math.GenericStruct>", + "Math.GenericStruct |(Math,Math.GenericStruct>)"), + }, "locals#fn_func_arr"); + + await CompareObjectPropertiesFor(locals, "fn_del_arr", new [] + { + TDelegate( + "Math.DelegateForSignatureTest", + "Math.GenericStruct DelegateTargetForSignatureTest (Math,Math.GenericStruct>)"), + TDelegate( + "Math.DelegateForSignatureTest", + "Math.GenericStruct |(Math,Math.GenericStruct>)") + }, "locals#fn_del_arr"); + + await CompareObjectPropertiesFor(locals, "fn_void_del_arr", new [] + { + TDelegate( + "Math.DelegateWithVoidReturn", + "void DelegateTargetWithVoidReturn (Math.GenericStruct)") + }, "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()); + + await CheckProps(locals, new + { + fn_action = TDelegate("System.Action>", + "void |(Math.GenericStruct)"), + fn_action_del = TDelegate("System.Action>", + "void DelegateTargetWithVoidReturn (Math.GenericStruct)"), + fn_action_bare = TDelegate("System.Action", + "void|()"), + + fn_action_null = TObject("System.Action>", is_null : true), + + fn_action_arr = TArray("System.Action>[]", 3), + + gs = TValueType("Math.GenericStruct"), + }, "locals"); + + await CompareObjectPropertiesFor(locals, "fn_action_arr", new [] + { + TDelegate( + "System.Action>", + "void |(Math.GenericStruct)"), + TDelegate( + "System.Action>", + "void DelegateTargetWithVoidReturn (Math.GenericStruct)"), + TObject("System.Action>", 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()); + + await CheckProps(locals, new + { + fn_func = TDelegate("System.Func, bool>", + "bool |(Func)"), + fn_func_null = TObject("System.Func, bool>", is_null : true), + fn_func_arr = TArray("System.Func, bool>[]", 1), + fn_del_arr = TArray("System.Func, bool>[]", 1), + + m_obj = TObject("Math"), + fn_del_null = TObject("System.Func, bool>", is_null : true), + fs = TDelegate("System.Func", + "bool |(int)") + }, "locals"); + + await CompareObjectPropertiesFor(locals, "fn_func_arr", new [] + { + TDelegate( + "System.Func, bool>", + "bool |(System.Func)") + }, "locals#fn_func_arr"); + + await CompareObjectPropertiesFor(locals, "fn_del_arr", new [] + { + TDelegate( + "System.Func, bool>", + "bool DelegateTargetForNestedFunc (Func)") + }, "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()); + + await CheckProps(locals, new + { + @this = TObject("Math"), + dst_arr = TArray("Math.DelegateForSignatureTest[]", 2), + fn_func = TDelegate("System.Func", + "bool |(char[])"), + fn_action = TDelegate("System.Action[]>", + "void |(Math.GenericStruct[])") + }, "locals"); + + await CompareObjectPropertiesFor(locals, "dst_arr", new [] + { + TDelegate("Math.DelegateForSignatureTest", + "Math.GenericStruct DelegateTargetForSignatureTest (Math,Math.GenericStruct>)"), + TDelegate("Math.DelegateForSignatureTest", + "Math.GenericStruct |(Math,Math.GenericStruct>)"), + }, "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()); + + await CheckProps(locals, new + { + @this = TObject("Math"), + _dst_arr = TArray("Math.DelegateForSignatureTest[]", 2), + _fn_func = TDelegate("System.Func", + "bool |(char[])"), + _fn_action = TDelegate("System.Action[]>", + "void |(Math.GenericStruct[])") + }, "locals"); + + await CompareObjectPropertiesFor(locals, "_dst_arr", new [] + { + TDelegate( + "Math.DelegateForSignatureTest", + "Math.GenericStruct DelegateTargetForSignatureTest (Math,Math.GenericStruct>)"), + TDelegate( + "Math.DelegateForSignatureTest", + "Math.GenericStruct |(Math,Math.GenericStruct>)"), + }, "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 index 0000000..bedba13 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/DevToolsClient.cs @@ -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 pending_ops = new List(); + TaskCompletionSource side_exit = new TaskCompletionSource(); + List pending_writes = new List(); + 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(pending_writes[0]), WebSocketMessageType.Text, true, token); + return current_write; + } + return null; + } + + async Task ReadOne(CancellationToken token) + { + byte[] buff = new byte[4000]; + var mem = new MemoryStream(); + while (true) + { + var result = await this.socket.ReceiveAsync(new ArraySegment(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(bytes), WebSocketMessageType.Text, true, token); + pending_ops.Add(current_write); + } + } + + async Task MarkCompleteAfterward(Func send, CancellationToken token) + { + try + { + await send(token); + side_exit.SetResult(true); + } + catch (Exception e) + { + side_exit.SetException(e); + } + } + + protected async Task ConnectWithMainLoops( + Uri uri, + Func receive, + Func 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) 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) 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 index 0000000..f2836d3 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/EvaluateOnCallFrameTests.cs @@ -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()); + var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a"); + CheckContentValue(evaluate, "1"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "b"); + CheckContentValue(evaluate, "2"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "c"); + CheckContentValue(evaluate, "3"); + + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "dt"); + await CheckDateTimeValue(evaluate, new DateTime(2000, 5, 4, 3, 2, 1)); + }); + + [Theory] + [InlineData(63, 12, "EvaluateTestsStructInstanceMethod")] + [InlineData(79, 12, "GenericInstanceMethodOnStruct")] + [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(), "a"); + CheckContentValue(evaluate, "1"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "b"); + CheckContentValue(evaluate, "2"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "c"); + CheckContentValue(evaluate, "3"); + + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "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()); + var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "g"); + CheckContentValue(evaluate, "100"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "h"); + CheckContentValue(evaluate, "200"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "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()); + var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "d"); + CheckContentValue(evaluate, "101"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "e"); + CheckContentValue(evaluate, "102"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "f"); + CheckContentValue(evaluate, "103"); + + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "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()); + + // sc_arg + { + var sc_arg = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "sc_arg"); + await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#1"); + + var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); + 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(), "local_gs"); + await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), "local_gs#1"); + + var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); + 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(), "local_gs"); + await CheckValue(local_gs, TValueType("DebuggerTests.SimpleGenericStruct"), "local_gs#2"); + + var local_gs_props = await GetProperties(local_gs["objectId"]?.Value()); + 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(), "sc_arg"); + await CheckValue(sc_arg, TObject("DebuggerTests.SimpleClass"), "sc_arg#2"); + + var sc_arg_props = await GetProperties(sc_arg["objectId"]?.Value()); + 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()); + var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "d + e"); + CheckContentValue(evaluate, "203"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "e + 10"); + CheckContentValue(evaluate, "112"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "a + a"); + CheckContentValue(evaluate, "2"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.a + this.b"); + CheckContentValue(evaluate, "3"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "\"test\" + \"test\""); + CheckContentValue(evaluate, "testtest"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "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()); + var evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.a"); + CheckContentValue(evaluate, "1"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.b"); + CheckContentValue(evaluate, "2"); + evaluate = await EvaluateOnCallFrame(pause_location["callFrames"][0]["callFrameId"].Value(), "this.c"); + CheckContentValue(evaluate, "3"); + + // FIXME: not supported yet + // evaluate = await EvaluateOnCallFrame (pause_location ["callFrames"][0] ["callFrameId"].Value (), "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 index 0000000..d62bfc2 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/InspectorClient.cs @@ -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) > pending_cmds = new List < (int, TaskCompletionSource) > (); + Func 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(), res["params"] as JObject, token); + var id = res["id"].Value(); + 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 onEvent, + Func send, + CancellationToken token) + { + + this.onEvent = onEvent; + await ConnectWithMainLoops(uri, HandleMessage, send, token); + } + + public Task 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(); + 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 index 0000000..e121870 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/PointerTests.cs @@ -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 PointersTestData => + new TheoryData + { { $"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()); + + 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()); + + 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()); + + 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()); + + 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*"), + gsp_null = TPointer("DebuggerTests.GenericStructWithUnmanagedT*") + }, "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"), "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"), "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()); + + 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()); + + var dt = new DateTime(5, 6, 7, 8, 9, 10); + await CheckProps(locals, new + { + gspa = TArray("DebuggerTests.GenericStructWithUnmanagedT*[]", 3), + }, "locals", num_fields : 26); + + // dtpa + var gspa_elems = await CompareObjectPropertiesFor(locals, "gspa", new [] + { + TPointer("DebuggerTests.GenericStructWithUnmanagedT*", is_null : true), + TPointer("DebuggerTests.GenericStructWithUnmanagedT*"), + TPointer("DebuggerTests.GenericStructWithUnmanagedT*"), + }); + { + var gs_dt = new DateTime(1, 2, 3, 4, 5, 6); + var actual_elems = await CheckArrayElements(gspa_elems, new [] + { + null, + TValueType("DebuggerTests.GenericStructWithUnmanagedT"), + TValueType("DebuggerTests.GenericStructWithUnmanagedT") + }); + + // *[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()); + + 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()); + + var dt = new DateTime(5, 6, 7, 8, 9, 10); + await CheckProps(locals, new + { + cwp = TObject("DebuggerTests.GenericClassWithPointers"), + cwp_null = TObject("DebuggerTests.GenericClassWithPointers") + }, "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 PointersAsMethodArgsTestData => + new TheoryData + { { $"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()); + + 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()); + + 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()); + var complex = GetAndAssertObjectWithName(locals, "complex"); + + // try to deref the non-pointer object, as a pointer + var props = await GetProperties(complex["value"]["objectId"].Value().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 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 index 0000000..b038d2c --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Support.cs @@ -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> notifications = new Dictionary>(); + Dictionary> eventListeners = new Dictionary>(); + + public const string PAUSE = "pause"; + public const string READY = "ready"; + + public Task 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(); + 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 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 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())) + { + 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 dicScriptsIdToUrl; + internal Dictionary dicFileToUrl; + internal Dictionary SubscribeToScripts(Inspector insp) + { + dicScriptsIdToUrl = new Dictionary(); + dicFileToUrl = new Dictionary(); + insp.On("Debugger.scriptParsed", async(args, c) => + { + var script_id = args?["scriptId"]?.Value(); + var url = args["url"]?.Value(); + if (script_id.StartsWith("dotnet://")) + { + var dbgUrl = args["dotNetUrl"]?.Value(); + var arrStr = dbgUrl.Split("/"); + dbgUrl = arrStr[0] + "/" + arrStr[1] + "/" + arrStr[2] + "/" + arrStr[arrStr.Length - 1]; + dicScriptsIdToUrl[script_id] = dbgUrl; + dicFileToUrl[dbgUrl] = args["url"]?.Value(); + } + 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 test_fn = null, Func 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()); + + 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 locals_fn = null, Func 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()); + + Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value()); + + 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()); + locals_fn(locals); + } + }); + } + + internal void CheckLocation(string script_loc, int line, int column, Dictionary scripts, JToken location) + { + var loc_str = $"{ scripts[location["scriptId"].Value()] }" + + $"#{ location ["lineNumber"].Value () }" + + $"#{ location ["columnNumber"].Value () }"; + + var expected_loc_str = $"{script_loc}#{line}#{column}"; + Assert.Equal(expected_loc_str, loc_str); + } + + internal void CheckNumber(JToken locals, string name, T value) + { + foreach (var l in locals) + { + if (name != l["name"]?.Value()) + continue; + var val = l["value"]; + Assert.Equal("number", val["type"]?.Value()); + Assert.Equal(value, val["value"].Value()); + 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()) + continue; + var val = l["value"]; + if (value == null) + { + Assert.Equal("object", val["type"]?.Value()); + Assert.Equal("null", val["subtype"]?.Value()); + } + else + { + Assert.Equal("string", val["type"]?.Value()); + Assert.Equal(value, val["value"]?.Value()); + } + 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()); + Assert.Equal(value, val["value"]?.Value()); + 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()); + Assert.True(val["isValueType"] == null || !val["isValueType"].Value()); + Assert.Equal(class_name, val["className"]?.Value()); + + var has_null_subtype = val["subtype"] != null && val["subtype"]?.Value() == "null"; + Assert.Equal(is_null, has_null_subtype); + if (subtype != null) + Assert.Equal(subtype, val["subtype"]?.Value()); + + return l; + } + + internal async Task 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(), "className"); + AssertEqual(expected.ToString(), value["description"]?.Value(), "description"); + + var members = await GetProperties(value["objectId"]?.Value()); + + // 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()); + if (val["value"] == null) + Assert.True(false, "expected bool value not found for variable named {name}"); + Assert.Equal(expected, val["value"]?.Value()); + + return l; + } + + internal void CheckContentValue(JToken token, string value) + { + var val = token["value"].Value(); + 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()); + Assert.True(val["isValueType"] != null && val["isValueType"].Value()); + Assert.Equal(class_name, val["className"]?.Value()); + 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()); + Assert.True(val["isEnum"] != null && val["isEnum"].Value()); + Assert.Equal(class_name, val["className"]?.Value()); + Assert.Equal(descr, val["description"]?.Value()); + return l; + } + + internal void CheckArray(JToken locals, string name, string class_name) + { + foreach (var l in locals) + { + if (name != l["name"]?.Value()) + continue; + + var val = l["value"]; + Assert.Equal("object", val["type"]?.Value()); + Assert.Equal("array", val["subtype"]?.Value()); + Assert.Equal(class_name, val["className"]?.Value()); + + //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() == name); + if (l == null) + Assert.True(false, $"Could not find variable '{name}'"); + return l; + } + + internal async Task 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 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()); + } + + // Place a breakpoint in the given method and run until its hit + // Return the Debugger.paused data + internal async Task 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 StepAndCheck(StepKind kind, string script_loc, int line, int column, string function_name, + Func wait_for_event_fn = null, Action 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 EvaluateAndCheck(string expression, string script_loc, int line, int column, string function_name, + Func wait_for_event_fn = null, Action 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 SendCommandAndCheck(JObject args, string method, string script_loc, int line, int column, string function_name, + Func wait_for_event_fn = null, Action 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()); + + 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()); + 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(), $"{label}-type"); + AssertEqual(exp_val["className"]?.Value(), actual_val["className"]?.Value(), $"{label}-className"); + + var actual_target = actual_val["description"]?.Value(); + Assert.True(actual_target != null, $"${label}-description"); + var exp_target = exp_val["target"].Value(); + + CheckDelegateTarget(actual_target, exp_target); + + var del_props = await GetProperties(actual_val["objectId"]?.Value()); + AssertEqual(1, del_props.Count(), $"${label}-delegate-properties-count"); + + var obj = del_props.Where(jt => jt["name"]?.Value() == "Target").FirstOrDefault(); + Assert.True(obj != null, $"[{label}] Property named 'Target' found found in delegate properties"); + + AssertEqual("symbol", obj["value"] ? ["type"]?.Value(), $"{label}#Target#type"); + CheckDelegateTarget(obj["value"] ? ["value"]?.Value(), 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(); + switch (ctype) + { + case "delegate": + await CheckDelegate(actual_val, exp_val, label); + break; + + case "pointer": + { + + if (exp_val["is_null"]?.Value() == true) + { + AssertEqual("symbol", actual_val["type"]?.Value(), $"{label}-type"); + + var exp_val_str = $"({exp_val ["type_name"]?.Value()}) 0"; + AssertEqual(exp_val_str, actual_val["value"]?.Value(), $"{label}-value"); + AssertEqual(exp_val_str, actual_val["description"]?.Value(), $"{label}-description"); + } + else if (exp_val["is_void"]?.Value() == true) + { + AssertEqual("symbol", actual_val["type"]?.Value(), $"{label}-type"); + + var exp_val_str = $"({exp_val ["type_name"]?.Value()})"; + AssertStartsWith(exp_val_str, actual_val["value"]?.Value(), $"{label}-value"); + AssertStartsWith(exp_val_str, actual_val["description"]?.Value(), $"{label}-description"); + } + else + { + AssertEqual("object", actual_val["type"]?.Value(), $"{label}-type"); + + var exp_prefix = $"({exp_val ["type_name"]?.Value()})"; + AssertStartsWith(exp_prefix, actual_val["className"]?.Value(), $"{label}-className"); + AssertStartsWith(exp_prefix, actual_val["description"]?.Value(), $"{label}-description"); + Assert.False(actual_val["className"]?.Value() == $"{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(), $"{label}-className"); + AssertStartsWith($"get {exp_val ["type_name"]?.Value ()} ()", get["description"]?.Value(), $"{label}-description"); + AssertEqual("function", get["type"]?.Value(), $"{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(), $"{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().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() == 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()); + await CheckProps(actual_props, exp_val, $"{label}-{exp_name}"); + } + else if (exp_val["__custom_type"] != null && exp_val["__custom_type"]?.Value() == "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()); + await CheckProps(new_val, exp_val, $"{label}-{actual_val["objectId"]?.Value()}"); + return; + } + + foreach (var jp in exp_val.Values()) + { + if (jp.Value.Type == JTokenType.Object) + { + var new_val = await GetProperties(actual_val["objectId"].Value()); + await CheckProps(new_val, jp.Value, $"{label}-{actual_val["objectId"]?.Value()}"); + + continue; + } + + var exp_val_str = jp.Value.Value(); + bool null_or_empty_exp_val = String.IsNullOrEmpty(exp_val_str); + + var actual_field_val = actual_val.Values().FirstOrDefault(a_jp => a_jp.Name == jp.Name); + var actual_field_val_str = actual_field_val?.Value?.Value(); + 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 ()}\n" + + $"Actual: {actual_field_val.Value.Value ()}"); + } + } + + internal async Task 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()); + + return await GetProperties(frame["callFrameId"].Value()); + } + + internal async Task GetObjectOnFrame(JToken frame, string name) + { + var locals = await GetProperties(frame["callFrameId"].Value()); + return await GetObjectOnLocals(locals, name); + } + + // Find an object with @name, *fetch* the object, and check against @o + internal async Task 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 GetObjectOnLocals(JToken locals, string name) + { + var obj = GetAndAssertObjectWithName(locals, name); + var objectId = obj["value"]["objectId"]?.Value(); + 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 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(); + } + + 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() == "length" && p["enumerable"]?.Value() != true) + { + p.Remove(); + break; + } + } + } + + return locals; + } + + internal async Task 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 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 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(); + var m_line = res.Value["result"]["line"].Value(); + + 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 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 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 b__11_0 (Math.GenericStruct)` + + .. pass target "as `target: "void |(Math.GenericStruct)"` + */ + 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 scripts; + + public bool UseCallFunctionOnBeforeGetProperties; + + public DebugTestContext(InspectorClient cli, Inspector insp, CancellationToken token, Dictionary 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 index 0000000..03964dd --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Tests.cs @@ -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()?.Count); + + var loc = bp1_res.Value["locations"]?.Value() [0]; + + Assert.NotNull(loc["scriptId"]); + Assert.Equal("dotnet://debugger-test.dll/debugger-test.cs", scripts[loc["scriptId"]?.Value()]); + 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()?.Count); + + var loc = bp1_res.Value["locations"]?.Value() [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()?.Count); + + var loc2 = bp2_res.Value["locations"]?.Value() [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()?.Count); + + var loc = bp1_res.Value["locations"]?.Value() [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()?.Count); + + var loc2 = bp2_res.Value["locations"]?.Value() [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()?.Count); + + var loc = bp1_res.Value["locations"]?.Value() [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()?.Count); + + var loc2 = bp2_res.Value["locations"]?.Value() [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()); + //Assert.Equal ((int)MonoErrorCodes.BpNotFound, bp1_res.Error ["code"]?.Value ()); + }); + } + + [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()); + Assert.Equal(bp.Value["breakpointId"]?.ToString(), pause_location["hitBreakpoints"] ? [0]?.Value()); + + var top_frame = pause_location["callFrames"][0]; + Assert.Equal("IntAdd", top_frame["functionName"].Value()); + Assert.Contains("debugger-test.cs", top_frame["url"].Value()); + + 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()); + }); + } + + [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(async() => await insp.WaitFor("Runtime.exceptionThrown")); + var ex_json = JObject.Parse(ex.Message); + Assert.Equal(dicFileToUrl["/debugger-driver.html"], ex_json["exceptionDetails"] ? ["url"]?.Value()); + }); + + } + + [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"); + CheckObject(locals, "list_null", "System.Collections.Generic.Dictionary", is_null : true); + + CheckArray(locals, "list_arr", "System.Collections.Generic.Dictionary[]"); + CheckObject(locals, "list_arr_null", "System.Collections.Generic.Dictionary[]", is_null : true); + + // Unused locals + CheckObject(locals, "list_unused", "System.Collections.Generic.Dictionary"); + CheckObject(locals, "list_null_unused", "System.Collections.Generic.Dictionary", is_null : true); + + CheckObject(locals, "list_arr_unused", "System.Collections.Generic.Dictionary[]"); + CheckObject(locals, "list_arr_null_unused", "System.Collections.Generic.Dictionary[]", 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()); + + 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()); + + 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"); + + 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()); + 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"); + 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"), + 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"); + } + + // 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#StringField"), + List = TObject("System.Collections.Generic.List", 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"), + 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"), + 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"), + 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"), + 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"), + 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"), + 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"); + } + + // ----------- 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()); + 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()); + 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()); + 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()); + 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()); + 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()); + await CheckProps(locals, new + { + ss_local = TObject("DebuggerTests.ValueTypesTest.SimpleStruct"), + gs_local = TValueType("DebuggerTests.ValueTypesTest.GenericStruct"), + 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"), + 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"), + 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#StringField"), + List = TObject("System.Collections.Generic.List"), + 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()); + 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()); + }); + } + + [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", + "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()); + + await CheckProps(frame_locals, new + { + sc_arg = TObject("DebuggerTests.SimpleClass"), + @this = TValueType("DebuggerTests.Point"), + local_gs = TValueType("DebuggerTests.SimpleGenericStruct") + }, + "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()); + + pause_location = await SendCommandAndCheck(null, $"Debugger.stepInto", null, -1, -1, null); + var top_frame = pause_location["callFrames"][0]; + + AssertEqual("WriteLine", top_frame["functionName"]?.Value(), "Expected to be in WriteLine method"); + var script_id = top_frame["functionLocation"]["scriptId"].Value(); + 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 index 0000000..0cda8d4 --- /dev/null +++ b/src/mono/wasm/debugger/DebuggerTestSuite/appsettings.json @@ -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 index 0000000..95e2fb1 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-array-test.cs @@ -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[] + { + null, + new GenericClass { Id = "gclass_arr#1#Id", Color = RGB.Red, Value = 5 }, + new GenericClass { Id = "gclass_arr#2#Id", Color = RGB.Blue, Value = -12 }, + }; + + var gclass_arr_empty = new GenericClass[0]; + GenericClass[] 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[] + { + new SimpleGenericStruct { 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 { 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[0]; + SimpleGenericStruct[] 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 ValueTypeLocalsAsync(bool call_other = false) + { + var gvclass_arr = new SimpleGenericStruct[] + { + new SimpleGenericStruct { 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 { 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[0]; + SimpleGenericStruct[] gvclass_arr_null = null; + Console.WriteLine($"ValueTypeLocalsAsync: call_other: {call_other}"); + SimpleGenericStruct gvclass; + Point[] points = null; + + if (call_other) + { + (gvclass, points) = await new ArrayTestsClass().InstanceMethodValueTypeLocalsAsync>(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 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[] + { + new SimpleGenericStruct + { + 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 + { + 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[0]; + SimpleGenericStruct[] 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 { 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 sc_arg) where T : SimpleClass + { + var local_gs = new SimpleGenericStruct { 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 + { + public string Id { get; set; } + public RGB Color { get; set; } + public T Value { get; set; } + } + + public struct SimpleGenericStruct + { + 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 index 0000000..ad3b12c --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-cfo-test.cs @@ -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 { 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 { 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> 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 index 0000000..8ea2650 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-datetime-test.cs @@ -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 index 0000000..049b6b6 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-driver.html @@ -0,0 +1,124 @@ + + + + + + + + + + + Stuff goes here + + + +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 index 0000000..4d958cd --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-evaluate-test.cs @@ -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(100, 200, "test"); + + var f_g_s = new EvaluateTestsGenericStruct(); + 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(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 + { + 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 index 0000000..122a080 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-pointers-test.cs @@ -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 { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp }; + var gs_null = new GenericStructWithUnmanagedT { 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*[] { null, gsp, gsp_null }; + + var cwp = new GenericClassWithPointers { Ptr = dtp }; + var cwp_null = new GenericClassWithPointers(); + 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 { Value = new DateTime(1, 2, 3, 4, 5, 6), IntField = 4, DTPP = &dtp }; + var gs_null = new GenericStructWithUnmanagedT { 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*[] { null, gsp, gsp_null }; + + var cwp = new GenericClassWithPointers { Ptr = dtp }; + var cwp_null = new GenericClassWithPointers(); + 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 where T : unmanaged + { + public T Value; + public int IntField; + + public DateTime** DTPP; + } + + public unsafe class GenericClassWithPointers 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 index 0000000..55a13d5 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-test.cs @@ -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 fn_func = (Math m) => m == null; + Func fn_func_null = null; + Func[] fn_func_arr = new Func[] { + (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 fn_func_unused = (Math m) => m == null; + Func fn_func_null_unused = null; + Func[] fn_func_arr_unused = new Func[] { (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(); + System.Collections.Generic.Dictionary list_null = null; + + var list_arr = new System.Collections.Generic.Dictionary[] { new System.Collections.Generic.Dictionary() }; + System.Collections.Generic.Dictionary[] 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(); + System.Collections.Generic.Dictionary list_null_unused = null; + + var list_arr_unused = new System.Collections.Generic.Dictionary[] { new System.Collections.Generic.Dictionary() }; + System.Collections.Generic.Dictionary[] 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 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 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>, GenericStruct> fn_func = (m, gs) => new GenericStruct(); + Func>, GenericStruct> fn_func_del = GenericStruct.DelegateTargetForSignatureTest; + Func>, GenericStruct> fn_func_null = null; + Func fn_func_only_ret = () => { Console.WriteLine ($"hello"); return true; }; + var fn_func_arr = new Func>, GenericStruct>[] { + (m, gs) => new GenericStruct () }; + + Math.DelegateForSignatureTest fn_del = GenericStruct.DelegateTargetForSignatureTest; + Math.DelegateForSignatureTest fn_del_l = (m, gs) => new GenericStruct { StringField = "fn_del_l#lambda" }; + var fn_del_arr = new Math.DelegateForSignatureTest[] { GenericStruct.DelegateTargetForSignatureTest, (m, gs) => new GenericStruct { StringField = "fn_del_arr#1#lambda" } }; + var m_obj = new Math(); + Math.DelegateForSignatureTest fn_del_null = null; + var gs_gs = new GenericStruct> + { + List = new System.Collections.Generic.List> + { + new GenericStruct { StringField = "gs#List#0#StringField" }, + new GenericStruct { 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[] + { + 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(); + 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> fn_action = (_) => { }; + Action> fn_action_del = Math.DelegateTargetWithVoidReturn; + Action fn_action_bare = () => { }; + Action> fn_action_null = null; + var fn_action_arr = new Action>[] + { + (gs) => new GenericStruct(), + Math.DelegateTargetWithVoidReturn, + null + }; + + var gs = new GenericStruct(); + fn_action(gs); + fn_action_del(gs); + fn_action_arr[0](gs); + fn_action_bare(); + OuterMethod(); + return 0; + } + + public static int NestedDelegatesTest() + { + Func, bool> fn_func = (_) => { return true; }; + Func, bool> fn_func_null = null; + var fn_func_arr = new Func, bool>[] { + (gs) => { return true; } }; + + var fn_del_arr = new Func, bool>[] { DelegateTargetForNestedFunc> }; + var m_obj = new Math(); + Func, bool> fn_del_null = null; + Func 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.DelegateTargetForSignatureTest, + (m, gs) => new GenericStruct() + }; + Func _fn_func = (cs) => cs.Length == 0; + Action[]> _fn_action = (gss) => { }; + + new Math().MethodWithDelegateArgs(_dst_arr, _fn_func, _fn_action); + } + + void MethodWithDelegateArgs(Math.DelegateForSignatureTest[] dst_arr, Func fn_func, + Action[]> 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.DelegateTargetForSignatureTest, + (m, gs) => new GenericStruct() + }; + Func _fn_func = (cs) => cs.Length == 0; + Action[]> _fn_action = (gss) => { }; + + Console.WriteLine($"Placeholder for breakpoint"); + await System.Threading.Tasks.Task.CompletedTask; + } + + public delegate void DelegateWithVoidReturn(GenericStruct gs); + public static void DelegateTargetWithVoidReturn(GenericStruct gs) { } + + delegate GenericStruct DelegateForSignatureTest(Math m, GenericStruct> gs); + static bool DelegateTargetForNestedFunc(T arg) => true; + + public struct SimpleStruct + { + public DateTime dt; + public GenericStruct gs; + } + + public struct GenericStruct + { + public System.Collections.Generic.List List; + public string StringField; + + public static GenericStruct DelegateTargetForSignatureTest(Math m, GenericStruct> gs) => new GenericStruct(); + } + +} + +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 index 0000000..310ec81 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-test.csproj @@ -0,0 +1,49 @@ + + + $(NetCoreAppCurrent) + wasm + Browser + $(ArtifactsBinDir)microsoft.netcore.app.runtime.browser-wasm\Release\runtimes\browser-wasm\ + $(MSBuildThisFileDirectory)obj\$(Configuration)\wasm + $(MSBuildThisFileDirectory)bin\$(Configuration)\publish + $(Configuration) + true + Library + 219 + portable + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/mono/wasm/debugger/tests/debugger-test2.cs b/src/mono/wasm/debugger/tests/debugger-test2.cs new file mode 100644 index 0000000..26817a2 --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-test2.cs @@ -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 index 0000000..3a4ceea --- /dev/null +++ b/src/mono/wasm/debugger/tests/debugger-valuetypes-test.cs @@ -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 { StringField = "gs_local#GenericStruct#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 MethodWithLocalStructsStaticAsync() + { + var ss_local = new SimpleStruct("set in MethodWithLocalStructsStaticAsync", 1, DateTimeKind.Utc); + var gs_local = new GenericStruct + { + StringField = "gs_local#GenericStruct#StringField", + List = new System.Collections.Generic.List { 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 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 + { + StringField = $"{str}#SimpleStruct#gs#StringField", + List = new System.Collections.Generic.List { new DateTime(2010 + f, 2 + f, 3 + f, 10 + f, 2 + f, 3 + f) }, + Options = Options.Option1 + }; + Kind = kind; + } + + public Task AsyncMethodWithStructArgs(GenericStruct gs) + { + Console.WriteLine($"placeholder line for a breakpoint"); + if (gs.List.Count > 0) + return Task.FromResult(true); + + return Task.FromResult(false); + } + } + + public struct GenericStruct + { + public System.Collections.Generic.List 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 index 0000000..3581a5a --- /dev/null +++ b/src/mono/wasm/debugger/tests/dependency.cs @@ -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 index 0000000..7ba8e66 --- /dev/null +++ b/src/mono/wasm/debugger/tests/other.js @@ -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 index 0000000..2e341d0 --- /dev/null +++ b/src/mono/wasm/debugger/tests/runtime-debugger.js @@ -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) + }, +}; diff --git a/src/mono/wasm/runtime/library_mono.js b/src/mono/wasm/runtime/library_mono.js index 093b9fd..0692c65 100644 --- a/src/mono/wasm/runtime/library_mono.js +++ b/src/mono/wasm/runtime/library_mono.js @@ -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: , 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') > 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: "", description: ""} }; + } + + 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; diff --git a/tools-local/tasks/mobile.tasks/WasmAppBuilder/WasmAppBuilder.cs b/tools-local/tasks/mobile.tasks/WasmAppBuilder/WasmAppBuilder.cs index a9a6ffc..4ca72c5 100644 --- a/tools-local/tasks/mobile.tasks/WasmAppBuilder/WasmAppBuilder.cs +++ b/tools-local/tasks/mobile.tasks/WasmAppBuilder/WasmAppBuilder.cs @@ -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 Assets { get; } = new List(); [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 nativeAssets = new List() { "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) {