diff --git a/Microsoft.DotNet.Interactive.FSharp.Tests/Microsoft.DotNet.Interactive.FSharp.Tests.fsproj b/Microsoft.DotNet.Interactive.FSharp.Tests/Microsoft.DotNet.Interactive.FSharp.Tests.fsproj index 6f79f8386..6237ece5c 100644 --- a/Microsoft.DotNet.Interactive.FSharp.Tests/Microsoft.DotNet.Interactive.FSharp.Tests.fsproj +++ b/Microsoft.DotNet.Interactive.FSharp.Tests/Microsoft.DotNet.Interactive.FSharp.Tests.fsproj @@ -2,6 +2,12 @@ netcoreapp3.0 + false + + + + + false diff --git a/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs b/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs index cc9c9de75..429709bcc 100644 --- a/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs +++ b/Microsoft.DotNet.Interactive.FSharp/FSharpKernel.fs @@ -16,7 +16,7 @@ type FSharpKernel() = let handleSubmitCode (codeSubmission: SubmitCode) (context: KernelInvocationContext) = async { let codeSubmissionReceived = CodeSubmissionReceived(codeSubmission.Code, codeSubmission) - context.OnNext(codeSubmissionReceived) + context.Publish(codeSubmissionReceived) let result, errors = try script.Eval(codeSubmission.Code) @@ -24,16 +24,16 @@ type FSharpKernel() = | ex -> Error(ex), [||] if errors.Length > 0 then let aggregateErrorMessage = System.String.Join("\n", errors) - context.OnNext(CommandFailed(aggregateErrorMessage, codeSubmission)) + context.Publish(CommandFailed(aggregateErrorMessage, codeSubmission)) match result with | Ok(Some(value)) -> let value = value.ReflectionValue let formattedValues = FormattedValue.FromObject(value) - context.OnNext(ReturnValueProduced(value, codeSubmission, formattedValues)) + context.Publish(ReturnValueProduced(value, codeSubmission, formattedValues)) | Ok(None) -> () | Error(ex) -> context.OnError(ex) - context.OnNext(CodeSubmissionEvaluated(codeSubmission)) - context.OnCompleted() + context.Publish(CodeSubmissionEvaluated(codeSubmission)) + context.Complete() } override __.HandleAsync(command: IKernelCommand, _context: KernelInvocationContext): Task = async { diff --git a/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs b/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs index c1300c330..f8fcb977a 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/KernelExtensions.cs @@ -42,7 +42,7 @@ private static T UseHtml(this T kernel) .Trim(); - context.OnNext(new Events.DisplayedValueProduced( + context.Publish(new Events.DisplayedValueProduced( htmlContent, context.Command, formattedValues: new[] @@ -50,7 +50,7 @@ private static T UseHtml(this T kernel) new FormattedValue("text/html", htmlContent) })); - context.OnCompleted(); + context.Complete(); } }) }); @@ -106,7 +106,7 @@ private static Command lsmagic() supportedDirectives.Commands.AddRange(context.CurrentKernel.Directives); - context.OnNext(new Events.DisplayedValueProduced(supportedDirectives)); + context.Publish(new Events.DisplayedValueProduced(supportedDirectives)); await context.CurrentKernel.VisitSubkernelsAsync(async k => { @@ -137,7 +137,7 @@ private static Command javascript() scriptContent)) .ToString(); - context.OnNext(new Events.DisplayedValueProduced( + context.Publish(new Events.DisplayedValueProduced( scriptContent, context.Command, formattedValues: new[] @@ -145,7 +145,7 @@ private static Command javascript() new FormattedValue("text/html", value) })); - context.OnCompleted(); + context.Complete(); } }) }; diff --git a/Microsoft.DotNet.Interactive.Rendering/PocketView.cs b/Microsoft.DotNet.Interactive.Rendering/PocketView.cs index a47ef33c4..0a0d1b57b 100644 --- a/Microsoft.DotNet.Interactive.Rendering/PocketView.cs +++ b/Microsoft.DotNet.Interactive.Rendering/PocketView.cs @@ -13,7 +13,7 @@ namespace Microsoft.DotNet.Interactive.Rendering { /// - /// Writes HTML using a C# DSL, bypasing the need for specialized parser and compiler infrastructure such as Razor or WebForms require. + /// Writes HTML using a C# DSL, bypassing the need for specialized parser and compiler infrastructure such as Razor or WebForms require. /// public class PocketView : DynamicObject, ITag { diff --git a/Microsoft.DotNet.Interactive/KernelBase.cs b/Microsoft.DotNet.Interactive/KernelBase.cs index 147a75153..1d0a380a4 100644 --- a/Microsoft.DotNet.Interactive/KernelBase.cs +++ b/Microsoft.DotNet.Interactive/KernelBase.cs @@ -109,7 +109,7 @@ private async Task HandleLoadExtension( await extension.OnLoadAsync(invocationContext.HandlingKernel); } - context.OnCompleted(); + context.Complete(); }; await next(loadExtension, invocationContext); @@ -155,8 +155,8 @@ private async Task HandleDirectivesAndSubmitCode( { submitCode.Handler = context => { - context.OnNext(new CodeSubmissionEvaluated(submitCode)); - context.OnCompleted(); + context.Publish(new CodeSubmissionEvaluated(submitCode)); + context.Complete(); return Task.CompletedTask; }; @@ -178,14 +178,14 @@ private async Task HandleDisplayValue( { displayValue.Handler = invocationContext => { - invocationContext.OnNext( + invocationContext.Publish( new Events.DisplayedValueProduced( displayValue.FormattedValue, displayValue, formattedValues: new[] { displayValue.FormattedValue }, valueId: displayValue.ValueId)); - invocationContext.OnCompleted(); + invocationContext.Complete(); return Task.CompletedTask; }; @@ -200,7 +200,7 @@ private async Task HandleUpdateDisplayValue( { displayedValue.Handler = invocationContext => { - invocationContext.OnNext( + invocationContext.Publish( new DisplayedValueUpdated( displayedValue.FormattedValue, valueId: displayedValue.ValueId, @@ -208,7 +208,7 @@ private async Task HandleUpdateDisplayValue( formattedValues: new[] { displayedValue.FormattedValue } )); - invocationContext.OnCompleted(); + invocationContext.Complete(); return Task.CompletedTask; }; diff --git a/Microsoft.DotNet.Interactive/KernelCommandPipeline.cs b/Microsoft.DotNet.Interactive/KernelCommandPipeline.cs index 836ad5d77..a530f6805 100644 --- a/Microsoft.DotNet.Interactive/KernelCommandPipeline.cs +++ b/Microsoft.DotNet.Interactive/KernelCommandPipeline.cs @@ -43,7 +43,7 @@ public async Task SendAsync( } catch (Exception exception) { - context.OnNext( + context.Publish( new CommandFailed( exception, command)); diff --git a/Microsoft.DotNet.Interactive/KernelInvocationContext.cs b/Microsoft.DotNet.Interactive/KernelInvocationContext.cs index 0b75fbc3d..e51cb61e5 100644 --- a/Microsoft.DotNet.Interactive/KernelInvocationContext.cs +++ b/Microsoft.DotNet.Interactive/KernelInvocationContext.cs @@ -11,7 +11,7 @@ namespace Microsoft.DotNet.Interactive { - public class KernelInvocationContext : IObserver, IDisposable + public class KernelInvocationContext : IDisposable { private readonly KernelInvocationContext _parentContext; private static readonly AsyncLocal> _currentStack = new AsyncLocal>(); @@ -30,7 +30,7 @@ private KernelInvocationContext( public IKernelCommand Command { get; } - public void OnCompleted() + public void Complete() { IsCompleted = true; _events.OnCompleted(); @@ -41,11 +41,11 @@ public void OnError(Exception exception) _events.OnError(exception); } - public void OnNext(IKernelEvent @event) + public void Publish(IKernelEvent @event) { if (_parentContext != null) { - _parentContext.OnNext(@event); + _parentContext.Publish(@event); } else { diff --git a/Microsoft.DotNet.Interactive/KernelStreamClient.cs b/Microsoft.DotNet.Interactive/KernelStreamClient.cs index 33d896afd..a0d8affb3 100644 --- a/Microsoft.DotNet.Interactive/KernelStreamClient.cs +++ b/Microsoft.DotNet.Interactive/KernelStreamClient.cs @@ -51,12 +51,12 @@ public Task Start() streamKernelCommand = obj.ToObject(); IKernelCommand command = null; - if (obj.TryGetValue("Command", out var commandValue)) + if (obj.TryGetValue("command", StringComparison.InvariantCultureIgnoreCase ,out var commandValue)) { command = DeserializeCommand(streamKernelCommand.CommandType, commandValue); } - if (streamKernelCommand.CommandType == "Quit") + if (streamKernelCommand.CommandType == nameof(Quit)) { return; } @@ -85,14 +85,6 @@ public Task Start() }, streamKernelCommand?.Id ?? -1); } - catch - { - Write(new CommandNotRecognized - { - Body = obj ?? (object)line - }, streamKernelCommand?.Id ?? -1); - } - } }); } @@ -102,7 +94,7 @@ private void Write(IKernelEvent e, int id) var wrapper = new StreamKernelEvent { Id = id, - Event = JsonConvert.SerializeObject(e), + Event = e, EventType = e.GetType().Name }; var serialized = JsonConvert.SerializeObject(wrapper, _jsonSerializerSettings); diff --git a/Microsoft.DotNet.Interactive/StreamKernelCommand.cs b/Microsoft.DotNet.Interactive/StreamKernelCommand.cs index 9c4750dd2..49cfa5987 100644 --- a/Microsoft.DotNet.Interactive/StreamKernelCommand.cs +++ b/Microsoft.DotNet.Interactive/StreamKernelCommand.cs @@ -1,9 +1,19 @@ -namespace Microsoft.DotNet.Interactive +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Newtonsoft.Json; + +namespace Microsoft.DotNet.Interactive { - public class StreamKernelCommand + internal class StreamKernelCommand { + [JsonProperty("id")] public int Id { get; set; } + + [JsonProperty("commandType")] public string CommandType { get; set; } + + [JsonProperty("command")] public object Command { get; set; } } } diff --git a/Microsoft.DotNet.Interactive/StreamKernelEvent.cs b/Microsoft.DotNet.Interactive/StreamKernelEvent.cs index e6013dfac..c332ab1ab 100644 --- a/Microsoft.DotNet.Interactive/StreamKernelEvent.cs +++ b/Microsoft.DotNet.Interactive/StreamKernelEvent.cs @@ -5,13 +5,15 @@ namespace Microsoft.DotNet.Interactive { - public class StreamKernelEvent + internal class StreamKernelEvent { [JsonProperty("id")] public int Id { get; set; } + [JsonProperty("eventType")] public string EventType { get; set; } + [JsonProperty("event")] - public string Event { get; set; } + public object Event { get; set; } } } diff --git a/Microsoft.DotNet.Interactive/StreamKernelExtensions.cs b/Microsoft.DotNet.Interactive/StreamKernelExtensions.cs index 314d33ae7..bdaf0060a 100644 --- a/Microsoft.DotNet.Interactive/StreamKernelExtensions.cs +++ b/Microsoft.DotNet.Interactive/StreamKernelExtensions.cs @@ -1,24 +1,32 @@ -using System.IO; +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; using System.Threading; using Microsoft.DotNet.Interactive.Commands; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace Microsoft.DotNet.Interactive { public static class StreamKernelExtensions { - static int id = 0; + private static int _id; + private static readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; - public static int WriteMessage(this StreamWriter writer, IKernelCommand command) + public static int WriteMessage(this StreamWriter writer, IKernelCommand command, int? correlationId = null) { - var message = new StreamKernelCommand() + var message = new StreamKernelCommand { - Id = Interlocked.Increment(ref id), + Id = correlationId?? Interlocked.Increment(ref _id), CommandType = command.GetType().Name, Command = command }; - writer.WriteLine(JsonConvert.SerializeObject(message)); + writer.WriteLine(JsonConvert.SerializeObject(message, _jsonSerializerSettings)); writer.Flush(); return message.Id; } diff --git a/WorkspaceServer.Tests/Kernel/KernelClientTests.Kernel_can_be_interacted_using_kernel_client.approved.json b/WorkspaceServer.Tests/Kernel/KernelClientTests.Kernel_can_be_interacted_using_kernel_client.approved.json new file mode 100644 index 000000000..b839010f0 --- /dev/null +++ b/WorkspaceServer.Tests/Kernel/KernelClientTests.Kernel_can_be_interacted_using_kernel_client.approved.json @@ -0,0 +1,7 @@ +{"id":1,"eventType":"CodeSubmissionReceived","event":{"code":"var x = 123;","value":"var x = 123;","command":{"code":"var x = 123;","targetKernelName":null,"submissionType":0}}} +{"id":1,"eventType":"CompleteCodeSubmissionReceived","event":{"code":"var x = 123;","command":{"code":"var x = 123;","targetKernelName":null,"submissionType":0}}} +{"id":1,"eventType":"CodeSubmissionEvaluated","event":{"code":"var x = 123;","command":{"code":"var x = 123;","targetKernelName":null,"submissionType":0}}} +{"id":2,"eventType":"CodeSubmissionReceived","event":{"code":"x","value":"x","command":{"code":"x","targetKernelName":null,"submissionType":0}}} +{"id":2,"eventType":"CompleteCodeSubmissionReceived","event":{"code":"x","command":{"code":"x","targetKernelName":null,"submissionType":0}}} +{"id":2,"eventType":"ReturnValueProduced","event":{"value":123,"formattedValues":[{"mimeType":"text/plain","value":"123"}],"valueId":null,"command":{"code":"x","targetKernelName":null,"submissionType":0}}} +{"id":2,"eventType":"CodeSubmissionEvaluated","event":{"code":"x","command":{"code":"x","targetKernelName":null,"submissionType":0}}} diff --git a/WorkspaceServer.Tests/Kernel/KernelClientTests.Kernel_client_surfaces_json_errors.approved.json b/WorkspaceServer.Tests/Kernel/KernelClientTests.Kernel_client_surfaces_json_errors.approved.json new file mode 100644 index 000000000..bd35e1da2 --- /dev/null +++ b/WorkspaceServer.Tests/Kernel/KernelClientTests.Kernel_client_surfaces_json_errors.approved.json @@ -0,0 +1 @@ +{"id":-1,"eventType":"CommandParseFailure","event":{"command":null,"body":"{ hello"}} diff --git a/WorkspaceServer.Tests/Kernel/KernelClientTests.cs b/WorkspaceServer.Tests/Kernel/KernelClientTests.cs index 884deb1d6..8362b36a3 100644 --- a/WorkspaceServer.Tests/Kernel/KernelClientTests.cs +++ b/WorkspaceServer.Tests/Kernel/KernelClientTests.cs @@ -2,16 +2,18 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; +using Assent; using FluentAssertions; using Microsoft.DotNet.Interactive; using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Events; using Microsoft.DotNet.Interactive.Tests; -using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using WorkspaceServer.Kernel; using Xunit; @@ -19,6 +21,16 @@ namespace WorkspaceServer.Tests.Kernel { public class KernelClientTests { + + private readonly Configuration _configuration; + + public KernelClientTests() + { + _configuration = new Configuration() + .UsingExtension("json"); + _configuration = _configuration.SetInteractive(Debugger.IsAttached); + } + [Fact] public async Task Kernel_can_be_interacted_using_kernel_client() { @@ -29,10 +41,9 @@ public async Task Kernel_can_be_interacted_using_kernel_client() var input = new MemoryStream(); var writer = new StreamWriter(input, Encoding.UTF8); - writer.WriteMessage(new SubmitCode(@"var x = -123;")); - writer.WriteMessage(new SubmitCode("x")); - writer.WriteMessage(new Quit()); + writer.WriteMessage(new SubmitCode(@"var x = 123;"), 1); + writer.WriteMessage(new SubmitCode("x"), 2); + writer.WriteMessage(new Quit(), 3); input.Position = 0; @@ -49,10 +60,7 @@ public async Task Kernel_can_be_interacted_using_kernel_client() var reader = new StreamReader(output, Encoding.UTF8); var text = reader.ReadToEnd(); - var events = text.Split(Environment.NewLine) - .Select(JsonConvert.DeserializeObject); - - events.Should().Contain(e => e.EventType == "ReturnValueProduced"); + this.Assent(text, _configuration); } [Fact] @@ -70,7 +78,7 @@ public async Task Kernel_client_surfaces_json_errors() var input = new MemoryStream(); var writer = new StreamWriter(input, Encoding.UTF8); writer.WriteLine("{ hello"); - writer.WriteMessage(new Quit()); + writer.WriteMessage(new Quit(), 2); writer.Flush(); input.Position = 0; @@ -88,10 +96,42 @@ public async Task Kernel_client_surfaces_json_errors() var reader = new StreamReader(output, Encoding.UTF8); var text = reader.ReadToEnd(); - var events = text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Select(JsonConvert.DeserializeObject); + this.Assent(text, _configuration); + } + + [Fact] + public async Task Kernel_client_surfaces_code_submission_Errors() + { + var kernel = new CSharpKernel(); - events.Should().Contain(e => e.EventType == "CommandParseFailure"); + var input = new MemoryStream(); + var writer = new StreamWriter(input, Encoding.UTF8); + writer.WriteMessage(new SubmitCode(@"var a = 12"), 1); + writer.WriteMessage(new Quit(), 2); + writer.Flush(); + + input.Position = 0; + + var output = new MemoryStream(); + + var streamKernel = new KernelStreamClient(kernel, + new StreamReader(input), + new StreamWriter(output)); + + var task = streamKernel.Start(); + await task; + + output.Position = 0; + var reader = new StreamReader(output, Encoding.UTF8); + + var text = reader.ReadToEnd(); + var events = text.Split(new[] {Environment.NewLine}, StringSplitOptions.RemoveEmptyEntries) + .Select(JObject.Parse).ToList(); + + events.Should() + .Contain(e => e["eventType"].Value() == nameof(IncompleteCodeSubmissionReceived)) + .And + .Contain(e => e["eventType"].Value() == nameof(CommandFailed)); } [Fact] @@ -104,8 +144,8 @@ public async Task Kernel_can_pound_r_nuget_using_kernel_client() var input = new MemoryStream(); var writer = new StreamWriter(input, Encoding.UTF8); - writer.WriteMessage(new SubmitCode(@"#r ""nuget:Microsoft.Spark, 0.4.0""")); - writer.WriteMessage(new Quit()); + writer.WriteMessage(new SubmitCode(@"#r ""nuget:Microsoft.Spark, 0.4.0"""), 1); + writer.WriteMessage(new Quit(), 2); input.Position = 0; @@ -123,13 +163,9 @@ public async Task Kernel_can_pound_r_nuget_using_kernel_client() var text = reader.ReadToEnd(); var events = text.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) - .Select(JsonConvert.DeserializeObject).ToList(); - - events.Should() - .Contain(e => e.EventType == nameof(NuGetPackageAdded)); + .Select(JObject.Parse).ToList(); - events.Should() - .NotContain(e => e.EventType == nameof(CommandNotRecognized)); + events.Should().Contain(e => e["eventType"].Value() == nameof(NuGetPackageAdded)); } } } \ No newline at end of file diff --git a/WorkspaceServer/Kernel/CSharpKernel.cs b/WorkspaceServer/Kernel/CSharpKernel.cs index c63049b30..f21f7a56e 100644 --- a/WorkspaceServer/Kernel/CSharpKernel.cs +++ b/WorkspaceServer/Kernel/CSharpKernel.cs @@ -105,17 +105,17 @@ private async Task HandleSubmitCode( submitCode.Code, submitCode); - context.OnNext(codeSubmissionReceived); + context.Publish(codeSubmissionReceived); var code = submitCode.Code; var isComplete = await IsCompleteSubmissionAsync(submitCode.Code); if(isComplete) { - context.OnNext(new CompleteCodeSubmissionReceived(submitCode)); + context.Publish(new CompleteCodeSubmissionReceived(submitCode)); } else { - context.OnNext(new IncompleteCodeSubmissionReceived(submitCode)); + context.Publish(new IncompleteCodeSubmissionReceived(submitCode)); } if (submitCode.SubmissionType == SubmissionType.Diagnose) @@ -164,24 +164,23 @@ private async Task HandleSubmitCode( compilationError.Diagnostics.Select(d => d.ToString())); } - context.OnNext(new CommandFailed(exception, submitCode, message)); - context.OnError(exception); + context.Publish(new CommandFailed(exception, submitCode, message)); + context.Complete(); } else { if (HasReturnValue) { var formattedValues = FormattedValue.FromObject(_scriptState.ReturnValue); - context.OnNext( + context.Publish( new ReturnValueProduced( _scriptState.ReturnValue, submitCode, formattedValues)); } - context.OnNext(new CodeSubmissionEvaluated(submitCode)); - - context.OnCompleted(); + context.Publish(new CodeSubmissionEvaluated(submitCode)); + context.Complete(); } } @@ -196,7 +195,7 @@ private void PublishOutput( PlainTextFormatter.MimeType, output) }; - context.OnNext( + context.Publish( new DisplayedValueProduced( output, command, @@ -210,12 +209,12 @@ private async Task HandleRequestCompletion( { var completionRequestReceived = new CompletionRequestReceived(requestCompletion); - context.OnNext(completionRequestReceived); + context.Publish(completionRequestReceived); var completionList = await GetCompletionList(requestCompletion.Code, requestCompletion.CursorPosition, scriptState); - context.OnNext(new CompletionRequestCompleted(completionList, requestCompletion)); + context.Publish(new CompletionRequestCompleted(completionList, requestCompletion)); } public void AddMetatadaReferences(IEnumerable references) diff --git a/WorkspaceServer/Kernel/CSharpKernelExtensions.cs b/WorkspaceServer/Kernel/CSharpKernelExtensions.cs index c6b32019d..354d97e16 100644 --- a/WorkspaceServer/Kernel/CSharpKernelExtensions.cs +++ b/WorkspaceServer/Kernel/CSharpKernelExtensions.cs @@ -66,8 +66,8 @@ public static CSharpKernel UseNugetDirective(this CSharpKernel kernel) kernel.AddMetatadaReferences(refs); } - context.OnNext(new NuGetPackageAdded(package)); - context.OnCompleted(); + context.Publish(new NuGetPackageAdded(package)); + context.Complete(); } };