[gh-88] Added initial exception visualization (`catch` only).
This commit is contained in:
Родитель
b9eaa800af
Коммит
7d8644575b
|
@ -2,10 +2,14 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpLab.Runtime.Internal {
|
||||
public static class Flow {
|
||||
private const int MaxReportLength = 20;
|
||||
private const int MaxReportItemCount = 3;
|
||||
|
||||
private static readonly List<Line> _lines = new List<Line>();
|
||||
public static IReadOnlyList<Line> Lines => _lines;
|
||||
|
||||
|
@ -22,7 +26,17 @@ namespace SharpLab.Runtime.Internal {
|
|||
AppendValue(notes, value);
|
||||
_lines[_lines.Count - 1] = line;
|
||||
}
|
||||
|
||||
|
||||
public static void ReportException(Exception exception) {
|
||||
var line = _lines[_lines.Count - 1];
|
||||
line.Exception = exception;
|
||||
_lines[_lines.Count - 1] = line;
|
||||
}
|
||||
|
||||
private static bool HadException() {
|
||||
return Marshal.GetExceptionPointers() != IntPtr.Zero || Marshal.GetExceptionCode() != 0;
|
||||
}
|
||||
|
||||
private static StringBuilder AppendValue<T>(StringBuilder builder, T value) {
|
||||
if (value == null)
|
||||
return builder.Append("null");
|
||||
|
@ -30,37 +44,57 @@ namespace SharpLab.Runtime.Internal {
|
|||
switch (value) {
|
||||
case IList<int> e: return AppendEnumerable(e, builder);
|
||||
case ICollection e: return AppendEnumerable(e.Cast<object>(), builder);
|
||||
default: return builder.Append(value);
|
||||
default: return AppendString(value.ToString(), builder);
|
||||
}
|
||||
}
|
||||
|
||||
private static StringBuilder AppendEnumerable<T>(IEnumerable<T> enumerable, StringBuilder builder) {
|
||||
builder.Append("{ ");
|
||||
var first = true;
|
||||
var index = 0;
|
||||
foreach (var item in enumerable) {
|
||||
if (!first) {
|
||||
if (index > 0)
|
||||
builder.Append(", ");
|
||||
}
|
||||
else {
|
||||
first = false;
|
||||
|
||||
if (index > MaxReportItemCount) {
|
||||
builder.Append("…");
|
||||
break;
|
||||
}
|
||||
|
||||
AppendValue(builder, item);
|
||||
index += 1;
|
||||
}
|
||||
builder.Append(" }");
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static StringBuilder AppendString(string value, StringBuilder builder) {
|
||||
var limit = MaxReportLength - builder.Length;
|
||||
if (limit <= 0)
|
||||
return builder;
|
||||
|
||||
if (value.Length <= limit) {
|
||||
builder.Append(value);
|
||||
}
|
||||
else {
|
||||
builder.Append(value, 0, limit);
|
||||
builder.Append("…");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public struct Line {
|
||||
private StringBuilder _notes;
|
||||
|
||||
public Line(int number) {
|
||||
_notes = null;
|
||||
Number = number;
|
||||
_notes = null;
|
||||
Exception = null;
|
||||
}
|
||||
|
||||
public int Number { get; }
|
||||
public Exception Exception { get; internal set; }
|
||||
|
||||
public bool HasNotes => _notes != null;
|
||||
public StringBuilder Notes {
|
||||
|
@ -69,7 +103,7 @@ namespace SharpLab.Runtime.Internal {
|
|||
_notes = new StringBuilder();
|
||||
return _notes;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,14 +70,17 @@ namespace SharpLab.Server.Execution {
|
|||
}
|
||||
|
||||
private void SerializeFlowLine(Flow.Line line, IFastJsonWriter writer) {
|
||||
if (!line.HasNotes) {
|
||||
if (!line.HasNotes && line.Exception == null) {
|
||||
writer.WriteValue(line.Number);
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartObject();
|
||||
writer.WriteProperty("line", line.Number);
|
||||
writer.WriteProperty("notes", line.Notes);
|
||||
if (line.HasNotes)
|
||||
writer.WriteProperty("notes", line.Notes);
|
||||
if (line.Exception != null)
|
||||
writer.WriteProperty("exception", line.Exception.GetType().Name);
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
|
|
|
@ -8,31 +8,36 @@ namespace SharpLab.Server.Execution.Internal {
|
|||
public class FlowReportingRewriter {
|
||||
private const int HiddenLine = 0xFEEFEE;
|
||||
|
||||
private static readonly MethodInfo ReportVariableMethod =
|
||||
typeof(Flow).GetMethod(nameof(Flow.ReportVariable));
|
||||
private static readonly MethodInfo ReportLineStartMethod =
|
||||
typeof(Flow).GetMethod(nameof(Flow.ReportLineStart));
|
||||
private static readonly MethodInfo ReportVariableMethod =
|
||||
typeof(Flow).GetMethod(nameof(Flow.ReportVariable));
|
||||
private static readonly MethodInfo ReportExceptionMethod =
|
||||
typeof(Flow).GetMethod(nameof(Flow.ReportException));
|
||||
|
||||
public void Rewrite(AssemblyDefinition assembly) {
|
||||
foreach (var module in assembly.Modules) {
|
||||
var reportVariable = module.ImportReference(ReportVariableMethod);
|
||||
var reportLineStart = module.ImportReference(ReportLineStartMethod);
|
||||
var flow = new ReportMethods {
|
||||
ReportLineStart = module.ImportReference(ReportLineStartMethod),
|
||||
ReportVariable = module.ImportReference(ReportVariableMethod),
|
||||
ReportException = module.ImportReference(ReportExceptionMethod),
|
||||
};
|
||||
foreach (var type in module.Types) {
|
||||
Rewrite(type, reportVariable, reportLineStart);
|
||||
Rewrite(type, flow);
|
||||
foreach (var nested in type.NestedTypes) {
|
||||
Rewrite(nested, reportVariable, reportLineStart);
|
||||
Rewrite(nested, flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Rewrite(TypeDefinition type, MethodReference reportVariable, MethodReference reportLineStart) {
|
||||
private void Rewrite(TypeDefinition type, ReportMethods flow) {
|
||||
foreach (var method in type.Methods) {
|
||||
Rewrite(method, reportVariable, reportLineStart);
|
||||
Rewrite(method, flow);
|
||||
}
|
||||
}
|
||||
|
||||
private void Rewrite(MethodDefinition method, MethodReference reportVariable, MethodReference reportLineStart) {
|
||||
private void Rewrite(MethodDefinition method, ReportMethods flow) {
|
||||
if (!method.HasBody || method.Body.Instructions.Count == 0)
|
||||
return;
|
||||
|
||||
|
@ -45,7 +50,7 @@ namespace SharpLab.Server.Execution.Internal {
|
|||
var sequencePoint = instruction.SequencePoint;
|
||||
if (sequencePoint != null && sequencePoint.StartLine != HiddenLine && sequencePoint.StartLine != lastLine) {
|
||||
InsertBefore(il, instruction, il.Create(OpCodes.Ldc_I4, sequencePoint.StartLine));
|
||||
InsertBefore(il, instruction, il.Create(OpCodes.Call, reportLineStart));
|
||||
InsertBefore(il, instruction, il.Create(OpCodes.Call, flow.ReportLineStart));
|
||||
i += 2;
|
||||
lastLine = sequencePoint.StartLine;
|
||||
}
|
||||
|
@ -61,10 +66,18 @@ namespace SharpLab.Server.Execution.Internal {
|
|||
var insertTarget = instruction;
|
||||
InsertAfter(il, ref insertTarget, ref i, il.Create(OpCodes.Ldstr, variable.Name));
|
||||
InsertAfter(il, ref insertTarget, ref i, il.Create(OpCodes.Ldloc, localIndex.Value));
|
||||
InsertAfter(il, ref insertTarget, ref i, il.Create(OpCodes.Call, new GenericInstanceMethod(reportVariable) {
|
||||
InsertAfter(il, ref insertTarget, ref i, il.Create(OpCodes.Call, new GenericInstanceMethod(flow.ReportVariable) {
|
||||
GenericArguments = { variable.VariableType }
|
||||
}));
|
||||
}
|
||||
|
||||
foreach (var handler in il.Body.ExceptionHandlers) {
|
||||
if (handler.HandlerType != ExceptionHandlerType.Catch)
|
||||
continue;
|
||||
var start = handler.HandlerStart;
|
||||
InsertBefore(il, start, il.Create(OpCodes.Dup));
|
||||
InsertBefore(il, start, il.Create(OpCodes.Call, flow.ReportException));
|
||||
}
|
||||
}
|
||||
|
||||
private void InsertAfter(ILProcessor il, ref Instruction target, ref int index, Instruction instruction) {
|
||||
|
@ -105,5 +118,11 @@ namespace SharpLab.Server.Execution.Internal {
|
|||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
private struct ReportMethods {
|
||||
public MethodReference ReportLineStart { get; set; }
|
||||
public MethodReference ReportVariable { get; set; }
|
||||
public MethodReference ReportException { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
using AshMind.Extensions;
|
||||
|
@ -7,6 +8,7 @@ using MirrorSharp;
|
|||
using MirrorSharp.Testing;
|
||||
using SharpLab.Server;
|
||||
using SharpLab.Server.MirrorSharp.Internal;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SharpLab.Tests {
|
||||
public class ExecutionTests {
|
||||
|
@ -15,12 +17,7 @@ namespace SharpLab.Tests {
|
|||
[Theory]
|
||||
[InlineData("Exceptions.CatchDivideByZero.cs")]
|
||||
public async Task SlowUpdate_ExecutesTryCatchWithoutErrors(string resourceName) {
|
||||
var code = EmbeddedResource.ReadAllText(typeof(ExecutionTests), "TestCode.Execution." + resourceName);
|
||||
var driver = MirrorSharpTestDriver.New(MirrorSharpOptions).SetText(code);
|
||||
await driver.SendSetOptionsAsync(new Dictionary<string, string> {
|
||||
{ "optimize", "debug" },
|
||||
{ "x-target", TargetNames.Run }
|
||||
});
|
||||
var driver = await NewTestDriverAsync(LoadCodeFromResource(resourceName));
|
||||
|
||||
var result = await driver.SendSlowUpdateAsync<ExecutionResultData>();
|
||||
var errors = result.JoinErrors();
|
||||
|
@ -29,8 +26,35 @@ namespace SharpLab.Tests {
|
|||
Assert.True(result.ExtensionResult.Exception.IsNullOrEmpty(), result.ExtensionResult.Exception);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Exceptions.CatchDivideByZero.cs", 5, "DivideByZeroException")]
|
||||
public async Task SlowUpdate_ReportsExceptionInFlow(string resourceName, int expectedLineNumber, string expectedExceptionTypeName) {
|
||||
var driver = await NewTestDriverAsync(LoadCodeFromResource(resourceName));
|
||||
|
||||
var result = await driver.SendSlowUpdateAsync<ExecutionResultData>();
|
||||
var lines = result.ExtensionResult.Flow
|
||||
.Select(f => new { Line = (f as JObject)?.Value<int>("line") ?? f.Value<int>(), Exception = (f as JObject)?.Value<string>("exception") })
|
||||
.ToArray();
|
||||
|
||||
Assert.Contains(new { Line = expectedLineNumber, Exception = expectedExceptionTypeName }, lines);
|
||||
}
|
||||
|
||||
private static string LoadCodeFromResource(string resourceName) {
|
||||
return EmbeddedResource.ReadAllText(typeof(ExecutionTests), "TestCode.Execution." + resourceName);
|
||||
}
|
||||
|
||||
private static async Task<MirrorSharpTestDriver> NewTestDriverAsync(string code) {
|
||||
var driver = MirrorSharpTestDriver.New(MirrorSharpOptions).SetText(code);
|
||||
await driver.SendSetOptionsAsync(new Dictionary<string, string> {
|
||||
{ "optimize", "debug" },
|
||||
{ "x-target", TargetNames.Run }
|
||||
});
|
||||
return driver;
|
||||
}
|
||||
|
||||
private class ExecutionResultData {
|
||||
public string Exception { get; set; }
|
||||
}
|
||||
public IList<JToken> Flow { get; } = new List<JToken>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export default function groupToMap(array, getKey) {
|
||||
const map = new Map();
|
||||
for (const item of array) {
|
||||
const key = getKey(item);
|
||||
let group = map.get(key);
|
||||
if (!group) {
|
||||
group = [];
|
||||
map.set(key, group);
|
||||
}
|
||||
group.push(item);
|
||||
}
|
||||
return map;
|
||||
}
|
|
@ -49,7 +49,7 @@
|
|||
this.root.setAttribute("height", this.sizer.offsetHeight);
|
||||
}
|
||||
|
||||
renderJump(fromLine, toLine) {
|
||||
renderJump(fromLine, toLine, options) {
|
||||
const key = fromLine + "->" + toLine;
|
||||
if (this.rendered[key])
|
||||
return;
|
||||
|
@ -71,7 +71,7 @@
|
|||
const toY = to.y - offsetY;
|
||||
|
||||
const g = this.renderSVG("g", {
|
||||
"class": "CodeMirror-flow-jump" + (up ? " CodeMirror-flow-jump-up" : ""),
|
||||
"class": "CodeMirror-flow-jump" + (up ? " CodeMirror-flow-jump-up" : "") + (options.throw ? " CodeMirror-flow-jump-throw" : ""),
|
||||
"data-debug": key
|
||||
});
|
||||
this.renderSVG(g, "path", {
|
||||
|
@ -136,14 +136,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("addFlowJump", function(fromLine, toLine) {
|
||||
CodeMirror.defineExtension("addFlowJump", function(fromLine, toLine, options) {
|
||||
/* eslint-disable no-invalid-this */
|
||||
let flow = this.state.flow;
|
||||
if (!flow) {
|
||||
flow = new FlowLayer(this);
|
||||
this.state.flow = flow;
|
||||
}
|
||||
flow.renderJump(fromLine, toLine);
|
||||
flow.renderJump(fromLine, toLine, options);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("clearFlowPoints", function() {
|
||||
|
|
|
@ -2,6 +2,7 @@ import Vue from 'vue';
|
|||
import mirrorsharp from 'mirrorsharp';
|
||||
import 'codemirror/mode/mllike/mllike';
|
||||
import '../codemirror/addon-flow.js';
|
||||
import groupToMap from '../../helpers/group-to-map.js';
|
||||
|
||||
Vue.component('app-mirrorsharp', {
|
||||
props: {
|
||||
|
@ -61,36 +62,44 @@ Vue.component('app-mirrorsharp', {
|
|||
});
|
||||
|
||||
const bookmarks = [];
|
||||
this.$watch('executionFlow', lines => {
|
||||
this.$watch('executionFlow', steps => {
|
||||
while (bookmarks.length > 0) {
|
||||
bookmarks.pop().clear();
|
||||
}
|
||||
|
||||
const cm = instance.getCodeMirror();
|
||||
cm.clearFlowPoints();
|
||||
if (!lines)
|
||||
if (!steps)
|
||||
return;
|
||||
|
||||
const notes = {};
|
||||
let lastLineNumber;
|
||||
for (const line of lines) {
|
||||
let lineNumber = line;
|
||||
if (typeof line === 'object') {
|
||||
lineNumber = line.line;
|
||||
(notes[lineNumber] || (notes[lineNumber] = [])).push(line.notes);
|
||||
let lastException;
|
||||
for (const step of steps) {
|
||||
let lineNumber = step;
|
||||
let exception = null;
|
||||
if (typeof step === 'object') {
|
||||
lineNumber = step.line;
|
||||
exception = step.exception;
|
||||
}
|
||||
|
||||
if (lastLineNumber != null && (lineNumber < lastLineNumber || lineNumber - lastLineNumber > 2))
|
||||
cm.addFlowJump(lastLineNumber - 1, lineNumber - 1);
|
||||
const important = (lastLineNumber != null && (lineNumber < lastLineNumber || lineNumber - lastLineNumber > 2)) || lastException;
|
||||
if (important)
|
||||
cm.addFlowJump(lastLineNumber - 1, lineNumber - 1, { throw: !!lastException });
|
||||
lastLineNumber = lineNumber;
|
||||
lastException = exception;
|
||||
}
|
||||
|
||||
for (const lineNumber in notes) {
|
||||
const cmLineNumber = parseInt(lineNumber) - 1;
|
||||
const noteWidget = createFlowLineNoteWidget(notes[lineNumber]);
|
||||
const detailsByLine = groupToMap(steps.filter(s => typeof s === 'object'), s => s.line);
|
||||
for (const [lineNumber, details] of detailsByLine) {
|
||||
const cmLineNumber = lineNumber - 1;
|
||||
const end = cm.getLine(cmLineNumber).length;
|
||||
const noteBookmark = cm.setBookmark({ line: cmLineNumber, ch: end }, { widget: noteWidget });
|
||||
bookmarks.push(noteBookmark);
|
||||
for (const partName of ['notes', 'exception']) {
|
||||
const parts = details.map(s => s[partName]).filter(p => p);
|
||||
if (!parts.length)
|
||||
continue;
|
||||
const widget = createFlowLineEndWidget(parts, partName);
|
||||
bookmarks.push(cm.setBookmark({ line: cmLineNumber, ch: end }, { widget }));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -98,9 +107,9 @@ Vue.component('app-mirrorsharp', {
|
|||
template: '<textarea></textarea>'
|
||||
});
|
||||
|
||||
function createFlowLineNoteWidget(notes) {
|
||||
function createFlowLineEndWidget(contents, kind) {
|
||||
const widget = document.createElement('span');
|
||||
widget.className = 'flow-line-end-note';
|
||||
widget.textContent = notes.join('; ');
|
||||
widget.className = 'flow-line-end flow-line-end-' + kind;
|
||||
widget.textContent = contents.join('; ');
|
||||
return widget;
|
||||
}
|
|
@ -41,10 +41,18 @@ textarea, .CodeMirror {
|
|||
background-color: @highlight-color;
|
||||
}
|
||||
|
||||
.flow-line-end-note {
|
||||
.flow-line-end {
|
||||
display: inline-block;
|
||||
padding: 0px 5px;
|
||||
margin-left: 5px;
|
||||
background: #eee;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
.flow-line-end-notes {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.flow-line-end-exception {
|
||||
background: @error-color;
|
||||
color: #fff;
|
||||
}
|
|
@ -11,15 +11,24 @@
|
|||
stroke: #b4cdce;
|
||||
}
|
||||
|
||||
.CodeMirror-flow-jump.CodeMirror-flow-jump-up {
|
||||
.CodeMirror-flow-jump-up {
|
||||
fill: #f7d1c5;
|
||||
stroke: #f7d1c5;
|
||||
}
|
||||
|
||||
.CodeMirror-flow-jump-throw {
|
||||
fill: #dc3912;
|
||||
stroke: #dc3912;
|
||||
}
|
||||
|
||||
.CodeMirror-flow-jump-line {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.CodeMirror-flow-jump-start {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.CodeMirror-flow-jump-throw .CodeMirror-flow-jump-start {
|
||||
fill: inherit;
|
||||
}
|
Загрузка…
Ссылка в новой задаче