[gh-88] Initial version of Dump()/Inspect().
This commit is contained in:
Родитель
09024f2b40
Коммит
b017b52b02
|
@ -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;
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче