[gh-88] Initial version of Dump()/Inspect().

This commit is contained in:
Andrey Shchekin 2017-07-22 19:50:38 +12:00
Родитель 09024f2b40
Коммит b017b52b02
13 изменённых файлов: 238 добавлений и 67 удалений

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

@ -1,7 +1,5 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
namespace SharpLab.Runtime.Internal { namespace SharpLab.Runtime.Internal {
@ -61,9 +59,9 @@ namespace SharpLab.Runtime.Internal {
return; return;
} }
AppendString(notes, name, ReportLimits.MaxVariableNameLength); ObjectAppender.AppendString(notes, name, ReportLimits.MaxVariableNameLength);
notes.Append(": "); notes.Append(": ");
AppendValue(notes, value); ObjectAppender.Append(notes, value, ReportLimits.MaxEnumerableItems, ReportLimits.MaxVariableValueLength);
// Have to reassign in case we set Notes // Have to reassign in case we set Notes
_steps[_steps.Count - 1] = step; _steps[_steps.Count - 1] = step;
} }
@ -74,48 +72,6 @@ namespace SharpLab.Runtime.Internal {
_steps[_steps.Count - 1] = step; _steps[_steps.Count - 1] = step;
} }
private static StringBuilder AppendValue<T>(StringBuilder builder, T value) {
if (value == null)
return builder.Append("null");
switch (value) {
case IList<int> e: return AppendEnumerable(builder, e);
case ICollection e: return AppendEnumerable(builder, e.Cast<object>());
default: return AppendString(builder, value.ToString(), ReportLimits.MaxVariableValueLength);
}
}
private static StringBuilder AppendEnumerable<T>(StringBuilder builder, IEnumerable<T> enumerable) {
builder.Append("{ ");
var index = 0;
foreach (var item in enumerable) {
if (index > 0)
builder.Append(", ");
if (index > ReportLimits.MaxEnumerableItems) {
builder.Append("…");
break;
}
AppendValue(builder, item);
index += 1;
}
builder.Append(" }");
return builder;
}
private static StringBuilder AppendString(StringBuilder builder, string value, int limit) {
if (value.Length <= limit) {
builder.Append(value);
}
else {
builder.Append(value, 0, limit - 1);
builder.Append("…");
}
return builder;
}
[Serializable] [Serializable]
public struct Step { public struct Step {
public Step(int lineNumber) { public Step(int lineNumber) {

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

@ -0,0 +1,15 @@
using System;
using System.Text;
namespace SharpLab.Runtime.Internal {
[Serializable]
public class InspectionResult {
public InspectionResult(string title, StringBuilder value) {
Title = title;
Value = value;
}
public string Title { get; }
public StringBuilder Value { get; }
}
}

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

@ -0,0 +1,55 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpLab.Runtime.Internal {
public static class ObjectAppender {
public static void Append<T>(StringBuilder builder, T value, int? maxEnumerableItemCount = null, int? maxValueLength = null) {
if (value == null) {
builder.Append("null");
return;
}
switch (value) {
case ICollection<int> c:
AppendEnumerable(builder, c, maxEnumerableItemCount, maxValueLength);
break;
case ICollection c:
AppendEnumerable(builder, c.Cast<object>(), maxEnumerableItemCount, maxValueLength);
break;
default:
AppendString(builder, value.ToString(), maxValueLength);
break;
}
}
private static void AppendEnumerable<T>(StringBuilder builder, IEnumerable<T> enumerable, int? maxItemCount, int? maxValueLength) {
builder.Append("{ ");
var index = 0;
foreach (var item in enumerable) {
if (index > 0)
builder.Append(", ");
if (index > maxItemCount) {
builder.Append("…");
break;
}
Append(builder, item, maxItemCount, maxValueLength);
index += 1;
}
builder.Append(" }");
}
public static void AppendString(StringBuilder builder, string value, int? maxLength) {
if (maxLength == null || value.Length <= maxLength) {
builder.Append(value);
}
else {
builder.Append(value, 0, maxLength.Value - 1);
builder.Append("…");
}
}
}
}

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

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace SharpLab.Runtime.Internal {
public static class Output {
private static readonly List<object> _stream = new List<object>();
public static IReadOnlyList<object> Stream => _stream;
public static void Write(InspectionResult data) {
_stream.Add(data);
}
public static void Write(string value) {
_stream.Add(value);
}
}
}

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

@ -0,0 +1,18 @@
using System.ComponentModel;
using System.Text;
using SharpLab.Runtime.Internal;
public static class ObjectExtensions {
// LinqPad/etc compatibility only
[EditorBrowsable(EditorBrowsableState.Never)]
public static void Dump<T>(this T value) {
value.Inspect(title: "Dump");
}
public static void Inspect<T>(this T value, string title = "Inspect") {
var builder = new StringBuilder();
ObjectAppender.Append(builder, value);
var data = new InspectionResult(title, builder);
Output.Write(data);
}
}

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

@ -5,17 +5,18 @@ using SharpLab.Runtime.Internal;
namespace SharpLab.Server.Execution { namespace SharpLab.Server.Execution {
[Serializable] [Serializable]
public class ExecutionResult { public class ExecutionResult {
public ExecutionResult(string returnValue, IReadOnlyList<Flow.Step> flow) { public ExecutionResult(IReadOnlyList<object> output, IReadOnlyList<Flow.Step> flow) {
ReturnValue = returnValue; Output = output;
Flow = flow; Flow = flow;
} }
public ExecutionResult(Exception exception, IReadOnlyList<Flow.Step> flow) { public ExecutionResult(Exception exception, IReadOnlyList<object> output, IReadOnlyList<Flow.Step> flow) {
Exception = exception; Exception = exception;
Output = output;
Flow = flow; Flow = flow;
} }
public string ReturnValue { get; } public IReadOnlyList<object> Output { get; }
public Exception Exception { get; } public Exception Exception { get; }
public IReadOnlyList<Flow.Step> Flow { get; } public IReadOnlyList<Flow.Step> Flow { get; }
} }

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

@ -55,12 +55,14 @@ namespace SharpLab.Server.Execution {
public void Serialize(ExecutionResult result, IFastJsonWriter writer) { public void Serialize(ExecutionResult result, IFastJsonWriter writer) {
writer.WriteStartObject(); writer.WriteStartObject();
if (result.Exception == null) { if (result.Exception != null)
writer.WriteProperty("returnValue", result.ReturnValue);
}
else {
writer.WriteProperty("exception", result.Exception.ToString()); writer.WriteProperty("exception", result.Exception.ToString());
writer.WritePropertyStartArray("output");
foreach (var item in result.Output) {
SerializeOutput(item, writer);
} }
writer.WriteEndArray();
writer.WritePropertyStartArray("flow"); writer.WritePropertyStartArray("flow");
foreach (var step in result.Flow) { foreach (var step in result.Flow) {
SerializeFlowStep(step, writer); SerializeFlowStep(step, writer);
@ -84,6 +86,27 @@ namespace SharpLab.Server.Execution {
writer.WriteEndObject(); writer.WriteEndObject();
} }
private void SerializeOutput(object item, IFastJsonWriter writer) {
switch (item) {
case InspectionResult inspection:
writer.WriteStartObject();
writer.WriteProperty("type", "inspection");
writer.WriteProperty("title", inspection.Title);
writer.WriteProperty("value", inspection.Value);
writer.WriteEndObject();
break;
case string s:
writer.WriteValue(s);
break;
case null:
writer.WriteValue("null");
break;
default:
writer.WriteValue("Unsupported output object type: " + item.GetType().Name);
break;
}
}
private static class Remote { private static class Remote {
public static ExecutionResult Execute(Stream assemblyStream, RuntimeGuardToken guardToken) { public static ExecutionResult Execute(Stream assemblyStream, RuntimeGuardToken guardToken) {
try { try {
@ -93,7 +116,9 @@ namespace SharpLab.Server.Execution {
using (guardToken.Scope()) { using (guardToken.Scope()) {
var result = m.Invoke(Activator.CreateInstance(c), null); var result = m.Invoke(Activator.CreateInstance(c), null);
return new ExecutionResult(result?.ToString(), Flow.Steps); if (m.ReturnType != typeof(void))
result.Inspect("Return");
return new ExecutionResult(Output.Stream, Flow.Steps);
} }
} }
catch (Exception ex) { catch (Exception ex) {
@ -101,7 +126,7 @@ namespace SharpLab.Server.Execution {
ex = invocationEx.InnerException; ex = invocationEx.InnerException;
Flow.ReportException(ex); Flow.ReportException(ex);
return new ExecutionResult(ex, Flow.Steps); return new ExecutionResult(ex, Output.Stream, Flow.Steps);
} }
} }

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

@ -2,6 +2,9 @@
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit; using Xunit;
using AshMind.Extensions; using AshMind.Extensions;
using Pedantic.IO; using Pedantic.IO;
@ -9,8 +12,7 @@ using MirrorSharp;
using MirrorSharp.Testing; using MirrorSharp.Testing;
using SharpLab.Server; using SharpLab.Server;
using SharpLab.Server.MirrorSharp.Internal; using SharpLab.Server.MirrorSharp.Internal;
using Newtonsoft.Json; using System;
using Newtonsoft.Json.Linq;
namespace SharpLab.Tests { namespace SharpLab.Tests {
public class ExecutionTests { public class ExecutionTests {
@ -57,8 +59,23 @@ namespace SharpLab.Tests {
Assert.Equal(expectedNotes, notes); Assert.Equal(expectedNotes, notes);
} }
private static int LineNumberFromFlowStep(JToken step) { [Theory]
return (step as JObject)?.Value<int>("line") ?? step.Value<int>(); [InlineData("3.Inspect();", "Inspect: 3")]
[InlineData("(1, 2, 3).Inspect();", "Inspect: (1, 2, 3)")]
[InlineData("new[] { 1, 2, 3 }.Inspect();", "Inspect: { 1, 2, 3 }")]
[InlineData("3.Dump();", "Dump: 3")]
public async Task SlowUpdate_IncludesExpectedOutput_ForInspectAndDump(string code, string expectedOutput) {
var driver = await NewTestDriverAsync(@"
public class C {
public void M() { " + code + @" }
}
");
var result = await driver.SendSlowUpdateAsync<ExecutionResultData>();
var errors = result.JoinErrors();
Assert.True(errors.IsNullOrEmpty(), errors);
Assert.Equal(expectedOutput, result.ExtensionResult.GetOutputAsString());
} }
private static string LoadCodeFromResource(string resourceName) { private static string LoadCodeFromResource(string resourceName) {
@ -80,6 +97,16 @@ namespace SharpLab.Tests {
public IList<FlowStepData> Flow { get; } = new List<FlowStepData>(); public IList<FlowStepData> Flow { get; } = new List<FlowStepData>();
[JsonProperty("flow")] [JsonProperty("flow")]
private IList<JToken> FlowRaw { get; } = new List<JToken>(); private IList<JToken> FlowRaw { get; } = new List<JToken>();
[JsonProperty]
private IList<JToken> Output { get; } = new List<JToken>();
public string GetOutputAsString() {
return string.Join("\n", Output.Select(token => {
if (token is JObject @object)
return @object.Value<string>("title") + ": " + @object.Value<string>("value");
return token.Value<string>();
}));
}
[OnDeserialized] [OnDeserialized]
private void OnDeserialized(StreamingContext context) { private void OnDeserialized(StreamingContext context) {

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

@ -106,14 +106,14 @@
</header> </header>
<div class="content"> <div class="content">
<div class="loader"></div> <div class="loader"></div>
<app-run-view class="output"
v-if="lastResultOfType.run"
v-show="result.type === 'run'"
v-bind:output="lastResultOfType.run.value.output"></app-run-view>
<app-mirrorsharp-readonly v-if="lastResultOfType.code" <app-mirrorsharp-readonly v-if="lastResultOfType.code"
v-show="result.type === 'code'" v-show="result.type === 'code'"
v-bind:value="lastResultOfType.code.value" v-bind:value="lastResultOfType.code.value"
v-bind:language="options.target"></app-mirrorsharp-readonly> v-bind:language="options.target"></app-mirrorsharp-readonly>
<app-run-view class="run-result"
v-if="lastResultOfType.run"
v-show="result.type === 'run'"
v-bind:model="lastResultOfType.run.value"></app-run-view>
<app-ast-view class="ast" <app-ast-view class="ast"
v-if="lastResultOfType.ast" v-if="lastResultOfType.ast"
v-show="result.type === 'ast'" v-show="result.type === 'ast'"

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

@ -2,7 +2,15 @@ import Vue from 'vue';
Vue.component('app-run-view', { Vue.component('app-run-view', {
props: { props: {
model: Object output: Array
}, },
template: `<div>{{model.returnValue}}</div>` template: `<div class="output">
<div v-for="item in output">
<div v-if="typeof item === 'string'">{{item}}</div>
<div v-if="typeof item === 'object' && item.type === 'inspection' && item.value" class="inspection inspection-value-only">
<h3>{{item.title}}:</h3>
<div class="inspection-value">{{item.value}}</div>
</div>
</div>
</div>`.replace(/[\r\n]+\s*/g, '').replace(/\s{2,}/g, ' ')
}); });

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

@ -209,4 +209,5 @@ section.info-only {
@import 'imports/mobile.less'; @import 'imports/mobile.less';
@import 'imports/ast.less'; @import 'imports/ast.less';
@import 'imports/codemirror.less'; @import 'imports/codemirror.less';
@import 'imports/output.less';
@import 'imports/offline.less'; @import 'imports/offline.less';

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

@ -17,7 +17,7 @@ option, optgroup {
border: 1px solid #fff; border: 1px solid #fff;
&::after { &::after {
content: '▾'; content: '\0025be'; /* ▾ */
position: absolute; position: absolute;
right: 2px; right: 2px;
top: 0; top: 0;

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

@ -0,0 +1,49 @@
@import 'common.less';
@inspection-border-color: #aaa;
@inspection-header-color: #aaa;
.output {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 2px 4px;
overflow: auto;
}
.output > * + * {
margin-top: 2px;
}
.inspection {
border: 1px solid @inspection-border-color;
}
.inspection-value-only {
display: flex;
flex-direction: row;
h3 {
min-width: 4.2em;
}
}
.inspection h3 {
font-size: inherit;
background-color: @inspection-header-color;
border-bottom: 1px solid @inspection-border-color;
color: #fff;
margin: 0;
padding: 2px 4px;
font-weight: normal;
}
.inspection-value {
text-align: right;
padding: 2px 4px;
}