[gh-88] Added initial exception visualization (`catch` only).

This commit is contained in:
Andrey Shchekin 2017-07-21 01:04:28 +12:00
Родитель b9eaa800af
Коммит 7d8644575b
9 изменённых файлов: 173 добавлений и 54 удалений

Просмотреть файл

@ -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;
}