[gh-88] Initial version of Dump()/Inspect().
This commit is contained in:
Родитель
09024f2b40
Коммит
b017b52b02
|
@ -1,7 +1,5 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpLab.Runtime.Internal {
|
||||
|
@ -61,9 +59,9 @@ namespace SharpLab.Runtime.Internal {
|
|||
return;
|
||||
}
|
||||
|
||||
AppendString(notes, name, ReportLimits.MaxVariableNameLength);
|
||||
ObjectAppender.AppendString(notes, name, ReportLimits.MaxVariableNameLength);
|
||||
notes.Append(": ");
|
||||
AppendValue(notes, value);
|
||||
ObjectAppender.Append(notes, value, ReportLimits.MaxEnumerableItems, ReportLimits.MaxVariableValueLength);
|
||||
// Have to reassign in case we set Notes
|
||||
_steps[_steps.Count - 1] = step;
|
||||
}
|
||||
|
@ -74,48 +72,6 @@ namespace SharpLab.Runtime.Internal {
|
|||
_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]
|
||||
public struct Step {
|
||||
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 {
|
||||
[Serializable]
|
||||
public class ExecutionResult {
|
||||
public ExecutionResult(string returnValue, IReadOnlyList<Flow.Step> flow) {
|
||||
ReturnValue = returnValue;
|
||||
public ExecutionResult(IReadOnlyList<object> output, IReadOnlyList<Flow.Step> flow) {
|
||||
Output = output;
|
||||
Flow = flow;
|
||||
}
|
||||
|
||||
public ExecutionResult(Exception exception, IReadOnlyList<Flow.Step> flow) {
|
||||
public ExecutionResult(Exception exception, IReadOnlyList<object> output, IReadOnlyList<Flow.Step> flow) {
|
||||
Exception = exception;
|
||||
Output = output;
|
||||
Flow = flow;
|
||||
}
|
||||
|
||||
public string ReturnValue { get; }
|
||||
public IReadOnlyList<object> Output { get; }
|
||||
public Exception Exception { get; }
|
||||
public IReadOnlyList<Flow.Step> Flow { get; }
|
||||
}
|
||||
|
|
|
@ -55,12 +55,14 @@ namespace SharpLab.Server.Execution {
|
|||
|
||||
public void Serialize(ExecutionResult result, IFastJsonWriter writer) {
|
||||
writer.WriteStartObject();
|
||||
if (result.Exception == null) {
|
||||
writer.WriteProperty("returnValue", result.ReturnValue);
|
||||
}
|
||||
else {
|
||||
if (result.Exception != null)
|
||||
writer.WriteProperty("exception", result.Exception.ToString());
|
||||
writer.WritePropertyStartArray("output");
|
||||
foreach (var item in result.Output) {
|
||||
SerializeOutput(item, writer);
|
||||
}
|
||||
writer.WriteEndArray();
|
||||
|
||||
writer.WritePropertyStartArray("flow");
|
||||
foreach (var step in result.Flow) {
|
||||
SerializeFlowStep(step, writer);
|
||||
|
@ -84,6 +86,27 @@ namespace SharpLab.Server.Execution {
|
|||
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 {
|
||||
public static ExecutionResult Execute(Stream assemblyStream, RuntimeGuardToken guardToken) {
|
||||
try {
|
||||
|
@ -93,7 +116,9 @@ namespace SharpLab.Server.Execution {
|
|||
|
||||
using (guardToken.Scope()) {
|
||||
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) {
|
||||
|
@ -101,7 +126,7 @@ namespace SharpLab.Server.Execution {
|
|||
ex = invocationEx.InnerException;
|
||||
|
||||
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.Runtime.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using AshMind.Extensions;
|
||||
using Pedantic.IO;
|
||||
|
@ -9,8 +12,7 @@ using MirrorSharp;
|
|||
using MirrorSharp.Testing;
|
||||
using SharpLab.Server;
|
||||
using SharpLab.Server.MirrorSharp.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
|
||||
namespace SharpLab.Tests {
|
||||
public class ExecutionTests {
|
||||
|
@ -57,8 +59,23 @@ namespace SharpLab.Tests {
|
|||
Assert.Equal(expectedNotes, notes);
|
||||
}
|
||||
|
||||
private static int LineNumberFromFlowStep(JToken step) {
|
||||
return (step as JObject)?.Value<int>("line") ?? step.Value<int>();
|
||||
[Theory]
|
||||
[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) {
|
||||
|
@ -80,6 +97,16 @@ namespace SharpLab.Tests {
|
|||
public IList<FlowStepData> Flow { get; } = new List<FlowStepData>();
|
||||
[JsonProperty("flow")]
|
||||
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]
|
||||
private void OnDeserialized(StreamingContext context) {
|
||||
|
|
|
@ -106,14 +106,14 @@
|
|||
</header>
|
||||
<div class="content">
|
||||
<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"
|
||||
v-show="result.type === 'code'"
|
||||
v-bind:value="lastResultOfType.code.value"
|
||||
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"
|
||||
v-if="lastResultOfType.ast"
|
||||
v-show="result.type === 'ast'"
|
||||
|
|
|
@ -2,7 +2,15 @@ import Vue from 'vue';
|
|||
|
||||
Vue.component('app-run-view', {
|
||||
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/ast.less';
|
||||
@import 'imports/codemirror.less';
|
||||
@import 'imports/output.less';
|
||||
@import 'imports/offline.less';
|
|
@ -17,7 +17,7 @@ option, optgroup {
|
|||
border: 1px solid #fff;
|
||||
|
||||
&::after {
|
||||
content: '▾';
|
||||
content: '\0025be'; /* ▾ */
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче