diff --git a/WorkspaceServer.Tests/AssertionExtensions.cs b/MLS.Agent.Tools.Tests/AssertionExtensions.cs similarity index 95% rename from WorkspaceServer.Tests/AssertionExtensions.cs rename to MLS.Agent.Tools.Tests/AssertionExtensions.cs index 8a730795a..9e9dfaca5 100644 --- a/WorkspaceServer.Tests/AssertionExtensions.cs +++ b/MLS.Agent.Tools.Tests/AssertionExtensions.cs @@ -1,13 +1,15 @@ // 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 FluentAssertions; +using System.Diagnostics; using System.Linq; +using FluentAssertions; using FluentAssertions.Collections; using FluentAssertions.Execution; -namespace WorkspaceServer.Tests +namespace MLS.Agent.Tools.Tests { + [DebuggerStepThrough] public static class AssertionExtensions { public static AndConstraint> BeEquivalentSequenceTo( diff --git a/MLS.Agent.Tools.Tests/RelativeDirectoryPathTests.cs b/MLS.Agent.Tools.Tests/RelativeDirectoryPathTests.cs index 40be0ce6e..b6515ce5f 100644 --- a/MLS.Agent.Tools.Tests/RelativeDirectoryPathTests.cs +++ b/MLS.Agent.Tools.Tests/RelativeDirectoryPathTests.cs @@ -3,7 +3,6 @@ using System; using FluentAssertions; -using MLS.Agent.Tools; using Xunit; namespace MLS.Agent.Tools.Tests diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/CompleteRequestHandlerTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/CompleteRequestHandlerTests.cs index a62b81836..642a7d949 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/CompleteRequestHandlerTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/CompleteRequestHandlerTests.cs @@ -3,52 +3,33 @@ using System; using System.Threading.Tasks; +using Clockwise; using FluentAssertions; using Microsoft.DotNet.Interactive.Jupyter.Protocol; -using WorkspaceServer.Kernel; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Interactive.Jupyter.Tests { - public class CompleteRequestHandlerTests + public class CompleteRequestHandlerTests : JupyterRequestHandlerTestBase { - private readonly MessageSender _ioPubChannel; - private readonly MessageSender _serverChannel; - private readonly RecordingSocket _serverRecordingSocket; - private readonly RecordingSocket _ioRecordingSocket; - private readonly KernelStatus _kernelStatus; - - public CompleteRequestHandlerTests() - { - var signatureValidator = new SignatureValidator("key", "HMACSHA256"); - _serverRecordingSocket = new RecordingSocket(); - _serverChannel = new MessageSender(_serverRecordingSocket, signatureValidator); - _ioRecordingSocket = new RecordingSocket(); - _ioPubChannel = new MessageSender(_ioRecordingSocket, signatureValidator); - _kernelStatus = new KernelStatus(); - } - - [Fact] - public void cannot_handle_requests_that_are_not_CompleteRequest() + public CompleteRequestHandlerTests(ITestOutputHelper output) : base(output) { - var kernel = new CSharpKernel(); - var handler = new CompleteRequestHandler(kernel); - var request = Message.Create(new DisplayData(), null); - Func messageHandling = () => handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); - messageHandling.Should().ThrowExactly(); } [Fact] public async Task send_completeReply_on_CompleteRequest() { - var kernel = new CSharpKernel(); - var handler = new CompleteRequestHandler(kernel); - var request = Message.Create(new CompleteRequest("System.Console.", 15 ), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + var scheduler = CreateScheduler(); + var request = Message.Create(new CompleteRequest("System.Console.", 15), null); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages - .Should().Contain(message => - message.Contains(MessageTypeValues.CompleteReply)); + .Should() + .Contain(message => + message.Contains(MessageTypeValues.CompleteReply)); } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/ExecuteRequestHandlerTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/ExecuteRequestHandlerTests.cs index 95e7a2138..66ae00c46 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/ExecuteRequestHandlerTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/ExecuteRequestHandlerTests.cs @@ -2,63 +2,32 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Reactive.Linq; using System.Threading.Tasks; +using Clockwise; using FluentAssertions; -using Microsoft.DotNet.Interactive.Commands; using Microsoft.DotNet.Interactive.Jupyter.Protocol; -using WorkspaceServer.Kernel; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Interactive.Jupyter.Tests { - public class ExecuteRequestHandlerTests + public class ExecuteRequestHandlerTests : JupyterRequestHandlerTestBase { - private readonly MessageSender _ioPubChannel; - private readonly MessageSender _serverChannel; - private readonly RecordingSocket _serverRecordingSocket; - private readonly RecordingSocket _ioRecordingSocket; - private readonly KernelStatus _kernelStatus; - - public ExecuteRequestHandlerTests() - { - var signatureValidator = new SignatureValidator("key", "HMACSHA256"); - _serverRecordingSocket = new RecordingSocket(); - _serverChannel = new MessageSender(_serverRecordingSocket, signatureValidator); - _ioRecordingSocket = new RecordingSocket(); - _ioPubChannel = new MessageSender(_ioRecordingSocket, signatureValidator); - _kernelStatus = new KernelStatus(); - } - - [Fact] - public void cannot_handle_requests_that_are_not_ExecuteRequest() + public ExecuteRequestHandlerTests(ITestOutputHelper output) : base(output) { - var kernel = new CSharpKernel(); - var handler = new ExecuteRequestHandler(kernel); - var request = Message.Create(new DisplayData(), null); - Func messageHandling = () => handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); - messageHandling.Should().ThrowExactly(); - } - - [Fact] - public async Task handles_executeRequest() - { - var kernel = new CSharpKernel(); - var handler = new ExecuteRequestHandler(kernel); - var request = Message.Create(new ExecuteRequest("var a =12;"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); } [Fact] public async Task sends_ExecuteInput_when_ExecuteRequest_is_handled() { - var kernel = new CSharpKernel(); - var handler = new ExecuteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new ExecuteRequest("var a =12;"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages - .Should().Contain(message => + .Should().Contain(message => message.Contains(MessageTypeValues.ExecuteInput)); _serverRecordingSocket.DecodedMessages @@ -69,11 +38,11 @@ public async Task sends_ExecuteInput_when_ExecuteRequest_is_handled() [Fact] public async Task sends_ExecuteReply_message_on_when_code_submission_is_handled() { - var kernel = new CSharpKernel(); - - var handler = new ExecuteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new ExecuteRequest("var a =12;"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages .Should().Contain(message => @@ -83,11 +52,11 @@ public async Task sends_ExecuteReply_message_on_when_code_submission_is_handled( [Fact] public async Task sends_ExecuteReply_with_error_message_on_when_code_submission_contains_errors() { - var kernel = new CSharpKernel(); - - var handler = new ExecuteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new ExecuteRequest("asdes"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages .Should() @@ -103,11 +72,11 @@ public async Task sends_ExecuteReply_with_error_message_on_when_code_submission_ [Fact] public async Task sends_DisplayData_message_on_ValueProduced() { - var kernel = new CSharpKernel(); - - var handler = new ExecuteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new ExecuteRequest("Console.WriteLine(2+2);"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages .Should().Contain(message => @@ -121,11 +90,11 @@ public async Task sends_DisplayData_message_on_ValueProduced() [Fact] public async Task sends_ExecuteReply_message_on_ReturnValueProduced() { - var kernel = new CSharpKernel(); - - var handler = new ExecuteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new ExecuteRequest("2+2"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages .Should().Contain(message => @@ -139,14 +108,11 @@ public async Task sends_ExecuteReply_message_on_ReturnValueProduced() [Fact] public async Task sends_ExecuteReply_message_when_submission_contains_only_a_directive() { - var kernel = new CompositeKernel - { - new CSharpKernel() - }.UseDefaultMagicCommands(); - - var handler = new ExecuteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new ExecuteRequest("%%csharp"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages .Should().Contain(message => diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/InterruptRequestHandlerTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/InterruptRequestHandlerTests.cs index 4fe4eb32a..cf4d94358 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/InterruptRequestHandlerTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/InterruptRequestHandlerTests.cs @@ -2,65 +2,36 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using Clockwise; +using FluentAssertions; using System.Linq; using System.Threading.Tasks; -using FluentAssertions; using Microsoft.DotNet.Interactive.Jupyter.Protocol; -using Recipes; -using WorkspaceServer.Kernel; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Interactive.Jupyter.Tests { - public class InterruptRequestHandlerTests + public class InterruptRequestHandlerTests : JupyterRequestHandlerTestBase { - private readonly MessageSender _ioPubChannel; - private readonly MessageSender _serverChannel; - private readonly RecordingSocket _serverRecordingSocket; - private readonly RecordingSocket _ioRecordingSocket; - private readonly KernelStatus _kernelStatus; - - public InterruptRequestHandlerTests() - { - var signatureValidator = new SignatureValidator("key", "HMACSHA256"); - _serverRecordingSocket = new RecordingSocket(); - _serverChannel = new MessageSender(_serverRecordingSocket, signatureValidator); - _ioRecordingSocket = new RecordingSocket(); - _ioPubChannel = new MessageSender(_ioRecordingSocket, signatureValidator); - _kernelStatus = new KernelStatus(); - } - - [Fact] - public void cannot_handle_requests_that_are_not_InterruptRequest() + public InterruptRequestHandlerTests(ITestOutputHelper output) : base(output) { - var kernel = new CSharpKernel(); - var handler = new InterruptRequestHandler(kernel); - var request = Message.Create(new DisplayData(), null); - Func messageHandling = () => handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); - messageHandling.Should().ThrowExactly(); - } - - [Fact] - public async Task handles_InterruptRequest() - { - var kernel = new CSharpKernel(); - var handler = new InterruptRequestHandler(kernel); - var request = Message.Create(new InterruptRequest(), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); } [Fact] public async Task sends_InterruptReply() { - var kernel = new CSharpKernel(); - var handler = new InterruptRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new InterruptRequest(), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await _kernelStatus.Idle(); - _serverRecordingSocket.DecodedMessages.SingleOrDefault(message => - message.Contains(MessageTypeValues.InterruptReply)) - .Should() - .NotBeNullOrWhiteSpace(); + _serverRecordingSocket.DecodedMessages + .SingleOrDefault(message => + message.Contains(MessageTypeValues.InterruptReply)) + .Should() + .NotBeNullOrWhiteSpace(); } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/IsCompleteRequestHandlerTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/IsCompleteRequestHandlerTests.cs index 27b59a551..3b852ba2b 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/IsCompleteRequestHandlerTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/IsCompleteRequestHandlerTests.cs @@ -2,91 +2,62 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using Clockwise; +using FluentAssertions; using System.Linq; using System.Threading.Tasks; -using FluentAssertions; using Microsoft.DotNet.Interactive.Jupyter.Protocol; +using Pocket; using Recipes; -using WorkspaceServer.Kernel; using Xunit; +using Xunit.Abstractions; namespace Microsoft.DotNet.Interactive.Jupyter.Tests { - public class IsCompleteRequestHandlerTests + public class IsCompleteRequestHandlerTests : JupyterRequestHandlerTestBase { - private readonly MessageSender _ioPubChannel; - private readonly MessageSender _serverChannel; - private readonly RecordingSocket _serverRecordingSocket; - private readonly RecordingSocket _ioRecordingSocket; - private readonly KernelStatus _kernelStatus; - - public IsCompleteRequestHandlerTests() - { - var signatureValidator = new SignatureValidator("key", "HMACSHA256"); - _serverRecordingSocket = new RecordingSocket(); - _serverChannel = new MessageSender(_serverRecordingSocket, signatureValidator); - _ioRecordingSocket = new RecordingSocket(); - _ioPubChannel = new MessageSender(_ioRecordingSocket, signatureValidator); - _kernelStatus = new KernelStatus(); - } - - [Fact] - public void cannot_handle_requests_that_are_not_ExecuteRequest() - { - var kernel = new CSharpKernel(); - var handler = new IsCompleteRequestHandler(kernel); - var request = Message.Create(new DisplayData(), null); - Func messageHandling = () => handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); - messageHandling.Should().ThrowExactly(); - } - - [Fact] - public async Task handles_isCompleteRequest() + public IsCompleteRequestHandlerTests(ITestOutputHelper output) : base(output) { - var kernel = new CSharpKernel(); - var handler = new IsCompleteRequestHandler(kernel); - var request = Message.Create(new IsCompleteRequest("var a =12;"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); } [Fact] public async Task sends_isCompleteReply_with_complete_if_the_code_is_a_complete_submission() { - var kernel = new CSharpKernel(); - var handler = new IsCompleteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new IsCompleteRequest("var a = 12;"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await _kernelStatus.Idle(); + + Logger.Log.Info("DecodedMessages: {messages}", _serverRecordingSocket.DecodedMessages); _serverRecordingSocket.DecodedMessages.SingleOrDefault(message => - message.Contains(MessageTypeValues.IsCompleteReply)) - .Should() - .NotBeNullOrWhiteSpace(); + message.Contains(MessageTypeValues.IsCompleteReply)) + .Should() + .NotBeNullOrWhiteSpace(); _serverRecordingSocket.DecodedMessages - .SingleOrDefault(m => m == new IsCompleteReply(string.Empty, "complete").ToJson()) - .Should() - .NotBeNullOrWhiteSpace(); - + .SingleOrDefault(m => m == new IsCompleteReply(string.Empty, "complete").ToJson()) + .Should() + .NotBeNullOrWhiteSpace(); } [Fact] public async Task sends_isCompleteReply_with_incomplete_and_indent_if_the_code_is_not_a_complete_submission() { - var kernel = new CSharpKernel(); - var handler = new IsCompleteRequestHandler(kernel); + var scheduler = CreateScheduler(); var request = Message.Create(new IsCompleteRequest("var a = 12"), null); - await handler.Handle(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); - + await scheduler.Schedule(new JupyterRequestContext(_serverChannel, _ioPubChannel, request, _kernelStatus)); + await _kernelStatus.Idle(); _serverRecordingSocket.DecodedMessages.SingleOrDefault(message => - message.Contains(MessageTypeValues.IsCompleteReply)) - .Should() - .NotBeNullOrWhiteSpace(); + message.Contains(MessageTypeValues.IsCompleteReply)) + .Should() + .NotBeNullOrWhiteSpace(); _serverRecordingSocket.DecodedMessages - .SingleOrDefault(m => m == new IsCompleteReply("*", "incomplete").ToJson()) - .Should() - .NotBeNullOrWhiteSpace(); - + .SingleOrDefault(m => m == new IsCompleteReply("*", "incomplete").ToJson()) + .Should() + .NotBeNullOrWhiteSpace(); } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterMessageContractTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterMessageContractTests.cs index 02c329c99..d05cf425c 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterMessageContractTests.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterMessageContractTests.cs @@ -27,8 +27,7 @@ public void KernelInfoReply_contract_has_not_been_broken() var socket = new TextSocket(); var sender = new MessageSender(socket, new SignatureValidator("key", "HMACSHA256")); var kernelInfoReply = new KernelInfoReply( - - "5.3", + Constants.VERSION, ".NET", "0.0.3", new LanguageInfo( diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterRequestHandlerTestBase{T}.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterRequestHandlerTestBase{T}.cs new file mode 100644 index 000000000..0847cf16f --- /dev/null +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/JupyterRequestHandlerTestBase{T}.cs @@ -0,0 +1,49 @@ +// 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; +using Clockwise; +using Microsoft.DotNet.Interactive.Jupyter.Protocol; +using Pocket; +using WorkspaceServer.Kernel; +using Xunit.Abstractions; + +namespace Microsoft.DotNet.Interactive.Jupyter.Tests +{ + public abstract class JupyterRequestHandlerTestBase : IDisposable + where T : JupyterMessageContent + { + private readonly CompositeDisposable _disposables =new CompositeDisposable(); + protected readonly MessageSender _ioPubChannel; + protected readonly MessageSender _serverChannel; + protected readonly RecordingSocket _serverRecordingSocket; + protected readonly RecordingSocket _ioRecordingSocket; + protected readonly KernelStatus _kernelStatus; + + protected JupyterRequestHandlerTestBase(ITestOutputHelper output) + { + _disposables.Add(output.SubscribeToPocketLogger()); + + var signatureValidator = new SignatureValidator("key", "HMACSHA256"); + _serverRecordingSocket = new RecordingSocket(); + _serverChannel = new MessageSender(_serverRecordingSocket, signatureValidator); + _ioRecordingSocket = new RecordingSocket(); + _ioPubChannel = new MessageSender(_ioRecordingSocket, signatureValidator); + _kernelStatus = new KernelStatus( + Header.Create(typeof(T), "test"), + _serverChannel); + } + + public void Dispose() => _disposables.Dispose(); + + protected ICommandScheduler CreateScheduler() + { + var handler = new JupyterRequestContextHandler(new CompositeKernel + { + new CSharpKernel() + }.UseDefaultMagicCommands()); + + return CommandScheduler.Create(handler.Handle).Trace(); + } + } +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/KernelStatus.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/KernelStatus.cs deleted file mode 100644 index ede4581fd..000000000 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/KernelStatus.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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. - -namespace Microsoft.DotNet.Interactive.Jupyter.Tests -{ - class KernelStatus : IRequestHandlerStatus - { - public void SetAsBusy() - { - IsBusy = true; - } - - public void SetAsIdle() - { - IsBusy = false; - } - - public bool IsBusy { get; private set; } - } -} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/KernelStatusTests.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/KernelStatusTests.cs new file mode 100644 index 000000000..4a03ead77 --- /dev/null +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/KernelStatusTests.cs @@ -0,0 +1,72 @@ +// 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.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using Microsoft.DotNet.Interactive.Jupyter.Protocol; +using MLS.Agent.Tools.Tests; +using Newtonsoft.Json.Linq; +using Recipes; +using Xunit; + +namespace Microsoft.DotNet.Interactive.Jupyter.Tests +{ + public class KernelStatusTests + { + private readonly KernelStatus _kernelStatus; + private readonly RecordingSocket _recordingSocket = new RecordingSocket(); + + public KernelStatusTests() + { + _kernelStatus = new KernelStatus( + Header.Create( + typeof(ExecuteRequest), "test"), + new MessageSender(_recordingSocket, new SignatureValidator("key", "HMACSHA256") + )); + } + + [Fact] + public void When_idle_then_awaiting_idle_returns_immediately() + { + var task = _kernelStatus.Idle(); + + task.IsCompleted.Should().BeTrue(); + } + + [Fact] + public async Task When_not_idle_then_idle_returns_after_SetAsIdle_is_called() + { + _kernelStatus.SetAsBusy(); + + var task = _kernelStatus.Idle(); + + task.IsCompleted.Should().BeFalse(); + + await Task.WhenAll( + Task.Run(() => _kernelStatus.SetAsIdle()), + task).Timeout(3.Seconds()); + } + + [Fact] + public void Status_message_is_only_sent_on_state_change() + { + _kernelStatus.SetAsBusy(); + _kernelStatus.SetAsBusy(); + _kernelStatus.SetAsIdle(); + _kernelStatus.SetAsIdle(); + _kernelStatus.SetAsBusy(); + + _recordingSocket + .DecodedMessages + .Where(m => m.StartsWith("{")) + .Select(JObject.Parse) + .SelectMany(jobj => jobj.Properties() + .Where(p => p.Name == "execution_state") + .Select(p => p.Value.Value())) + .Should() + .BeEquivalentSequenceTo("idle", "busy", "idle", "busy"); + } + } +} \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj b/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj index 2f234702e..64744bd99 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/Microsoft.DotNet.Interactive.Jupyter.Tests.csproj @@ -39,6 +39,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers + diff --git a/Microsoft.DotNet.Interactive.Jupyter.Tests/RecordingSocket.cs b/Microsoft.DotNet.Interactive.Jupyter.Tests/RecordingSocket.cs index 2d71c0bd7..7c77d5ef8 100644 --- a/Microsoft.DotNet.Interactive.Jupyter.Tests/RecordingSocket.cs +++ b/Microsoft.DotNet.Interactive.Jupyter.Tests/RecordingSocket.cs @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Interactive.Jupyter.Tests { - internal class RecordingSocket : IOutgoingSocket + public class RecordingSocket : IOutgoingSocket { public List DecodedMessages { get; } = new List(); diff --git a/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs index 80a24ff0a..b1d463205 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/CompleteRequestHandler.cs @@ -27,7 +27,7 @@ public async Task Handle(JupyterRequestContext context) { var completeRequest = GetJupyterRequest(context); - context.RequestHandlerStatus.SetAsBusy(); + context.KernelStatus.SetAsBusy(); var command = new RequestCompletion(completeRequest.Code, completeRequest.CursorPosition); @@ -63,7 +63,7 @@ private static void OnCompletionRequestCompleted(CompletionRequestCompleted comp var completeReply = Message.CreateResponse(reply, openRequest.Context.Request); openRequest.Context.ServerChannel.Send(completeReply); - openRequest.Context.RequestHandlerStatus.SetAsIdle(); + openRequest.Context.KernelStatus.SetAsIdle(); } private static int ComputeReplacementStartPosition(string code, int cursorPosition) diff --git a/Microsoft.DotNet.Interactive.Jupyter/Constants.cs b/Microsoft.DotNet.Interactive.Jupyter/Constants.cs index a59ce75b1..1d4e6bbf6 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/Constants.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/Constants.cs @@ -7,7 +7,7 @@ public static class Constants { public const string USERNAME = "dotnet_kernel"; - public const string VERSION = "5.0"; + public const string VERSION = "5.3"; public const string DELIMITER = ""; } diff --git a/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs index 757083f34..426b02b06 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/ExecuteRequestHandler.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.Interactive.Commands; @@ -27,7 +28,7 @@ public async Task Handle(JupyterRequestContext context) { var executeRequest = GetJupyterRequest(context); - context.RequestHandlerStatus.SetAsBusy(); + context.KernelStatus.SetAsBusy(); var executionCount = executeRequest.Silent ? _executionCount : Interlocked.Increment(ref _executionCount); var executeInputPayload = new ExecuteInput(executeRequest.Code, executionCount); @@ -114,7 +115,7 @@ private void OnCommandFailed(CommandFailed commandFailed) openRequest.Context.Request); openRequest.Context.ServerChannel.Send(executeReply); - openRequest.Context.RequestHandlerStatus.SetAsIdle(); + openRequest.Context.KernelStatus.SetAsIdle(); } private void SendDisplayData(DisplayData displayData, InflightRequest openRequest) @@ -199,7 +200,7 @@ private void OnCommandHandled(CommandHandled commandHandled) openRequest.Context.Request); openRequest.Context.ServerChannel.Send(executeReply); - openRequest.Context.RequestHandlerStatus.SetAsIdle(); + openRequest.Context.KernelStatus.SetAsIdle(); } } } diff --git a/Microsoft.DotNet.Interactive.Jupyter/IRequestHandlerStatus.cs b/Microsoft.DotNet.Interactive.Jupyter/IKernelStatus.cs similarity index 76% rename from Microsoft.DotNet.Interactive.Jupyter/IRequestHandlerStatus.cs rename to Microsoft.DotNet.Interactive.Jupyter/IKernelStatus.cs index 4bb45bccc..46d6da352 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/IRequestHandlerStatus.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/IKernelStatus.cs @@ -1,11 +1,16 @@ // 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.Threading.Tasks; + namespace Microsoft.DotNet.Interactive.Jupyter { - public interface IRequestHandlerStatus + public interface IKernelStatus { void SetAsBusy(); + void SetAsIdle(); + + Task Idle(); } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter/InterruptRequestHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/InterruptRequestHandler.cs index 189eb049e..ccb15785c 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/InterruptRequestHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/InterruptRequestHandler.cs @@ -40,7 +40,7 @@ private void OnExecutionInterrupted(CurrentCommandCancelled currentCommandCancel openRequest.Context.Request); openRequest.Context.ServerChannel.Send(interruptReply); - openRequest.Context.RequestHandlerStatus.SetAsIdle(); + openRequest.Context.KernelStatus.SetAsIdle(); } } @@ -48,7 +48,7 @@ public async Task Handle(JupyterRequestContext context) { var interruptRequest = GetJupyterRequest(context); - context.RequestHandlerStatus.SetAsBusy(); + context.KernelStatus.SetAsBusy(); var command = new CancelCurrentCommand(); diff --git a/Microsoft.DotNet.Interactive.Jupyter/IsCompleteRequestHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/IsCompleteRequestHandler.cs index 2f1f2f088..dc827cc72 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/IsCompleteRequestHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/IsCompleteRequestHandler.cs @@ -59,7 +59,7 @@ private void OnKernelEvent(IKernelEvent @event, bool isComplete) openRequest.Context.Request); openRequest.Context.ServerChannel.Send(executeReply); - openRequest.Context.RequestHandlerStatus.SetAsIdle(); + openRequest.Context.KernelStatus.SetAsIdle(); } } } diff --git a/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContext.cs b/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContext.cs index 01bddedc6..2df148414 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContext.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContext.cs @@ -17,15 +17,15 @@ public T GetRequestContent() where T : JupyterMessageContent return Request?.Content as T; } - public IRequestHandlerStatus RequestHandlerStatus { get; } + public IKernelStatus KernelStatus { get; } public JupyterRequestContext(IMessageSender serverChannel, IMessageSender ioPubChannel, Message request, - IRequestHandlerStatus requestHandlerStatus) + IKernelStatus kernelStatus) { ServerChannel = serverChannel ?? throw new ArgumentNullException(nameof(serverChannel)); IoPubChannel = ioPubChannel ?? throw new ArgumentNullException(nameof(ioPubChannel)); Request = request ?? throw new ArgumentNullException(nameof(request)); - RequestHandlerStatus = requestHandlerStatus; + KernelStatus = kernelStatus; } } } \ No newline at end of file diff --git a/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContextHandler.cs b/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContextHandler.cs index 3672687c9..aa5fcf9c5 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContextHandler.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/JupyterRequestContextHandler.cs @@ -12,7 +12,6 @@ namespace Microsoft.DotNet.Interactive.Jupyter { public class JupyterRequestContextHandler : ICommandHandler { - private readonly ExecuteRequestHandler _executeHandler; private readonly CompleteRequestHandler _completeHandler; private readonly InterruptRequestHandler _interruptHandler; @@ -31,7 +30,6 @@ public JupyterRequestContextHandler( _completeHandler = new CompleteRequestHandler(kernel, scheduler); _interruptHandler = new InterruptRequestHandler(kernel, scheduler); _isCompleteHandler = new IsCompleteRequestHandler(kernel, scheduler); - } public async Task Handle( @@ -51,6 +49,8 @@ public async Task Handle( case MessageTypeValues.IsCompleteRequest: await _isCompleteHandler.Handle(delivery.Command); break; + default: + break; } return delivery.Complete(); diff --git a/Microsoft.DotNet.Interactive.Jupyter/RequestHandlerStatus.cs b/Microsoft.DotNet.Interactive.Jupyter/KernelStatus.cs similarity index 51% rename from Microsoft.DotNet.Interactive.Jupyter/RequestHandlerStatus.cs rename to Microsoft.DotNet.Interactive.Jupyter/KernelStatus.cs index 9a4a96af4..99691003c 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/RequestHandlerStatus.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/KernelStatus.cs @@ -1,30 +1,36 @@ // 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; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; using Microsoft.DotNet.Interactive.Jupyter.Protocol; namespace Microsoft.DotNet.Interactive.Jupyter { - internal class RequestHandlerStatus : IRequestHandlerStatus + public class KernelStatus : IKernelStatus { private readonly Header _requestHeader; private readonly MessageSender _messageSender; + private readonly BehaviorSubject _idleState = new BehaviorSubject(true); - public RequestHandlerStatus(Header requestHeader, MessageSender messageSender) + public KernelStatus(Header requestHeader, MessageSender messageSender) { _requestHeader = requestHeader; _messageSender = messageSender; - } - public void SetAsBusy() - { - SetStatus(StatusValues.Busy); - } - public void SetAsIdle() - { - SetStatus(StatusValues.Idle); + _idleState + .DistinctUntilChanged() + .Subscribe(value => SetStatus(value ? StatusValues.Idle : StatusValues.Busy)); } + public void SetAsBusy() => _idleState.OnNext(false); + + public void SetAsIdle() => _idleState.OnNext(true); + + public async Task Idle() => await _idleState.FirstAsync(value => value); + private void SetStatus(string status) { var content = new Status(status); diff --git a/Microsoft.DotNet.Interactive.Jupyter/Protocol/Header.cs b/Microsoft.DotNet.Interactive.Jupyter/Protocol/Header.cs index d1676e51f..84bd5451f 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/Protocol/Header.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/Protocol/Header.cs @@ -58,7 +58,13 @@ public static Header Create(Type messageType, string session) public static Header Create(string messageType, string session) { - var newHeader = new Header(messageType: messageType, messageId: Guid.NewGuid().ToString(), version: "5.3", username: Constants.USERNAME, session: session, date: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")); + var newHeader = new Header( + messageType: messageType, + messageId: Guid.NewGuid().ToString(), + version: Constants.VERSION, + username: Constants.USERNAME, + session: session, + date: DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")); return newHeader; } diff --git a/Microsoft.DotNet.Interactive.Jupyter/Shell.cs b/Microsoft.DotNet.Interactive.Jupyter/Shell.cs index f1bb78fbb..71e7c0af1 100644 --- a/Microsoft.DotNet.Interactive.Jupyter/Shell.cs +++ b/Microsoft.DotNet.Interactive.Jupyter/Shell.cs @@ -80,8 +80,7 @@ public async Task StartAsync(CancellationToken cancellationToken) activity.Info("Received: {message}", message.ToJson()); - var status = new RequestHandlerStatus(message.Header, new MessageSender(_ioPubSocket, _signatureValidator)); - + var status = new KernelStatus(message.Header, new MessageSender(_ioPubSocket, _signatureValidator)); switch (message.Header.MessageType) { @@ -101,7 +100,7 @@ public async Task StartAsync(CancellationToken cancellationToken) _shellSender, _ioPubSender, message, - new RequestHandlerStatus(message.Header, _shellSender)); + new KernelStatus(message.Header, _shellSender)); await _scheduler.Schedule(context);