diff --git a/MLS.Agent.Tests/CommandLine/CommandLineParserTests.cs b/MLS.Agent.Tests/CommandLine/CommandLineParserTests.cs index 6e8ab05c3..440a297ba 100644 --- a/MLS.Agent.Tests/CommandLine/CommandLineParserTests.cs +++ b/MLS.Agent.Tests/CommandLine/CommandLineParserTests.cs @@ -383,7 +383,7 @@ public async Task jupyter_returns_error_if_connection_file_path_does_not_exits() testConsole.Error.ToString().Should().Contain("File does not exist: not_exist.json"); } - [Fact] + [Fact(Skip ="Skipped until System.CommandLine allows subcommands to skip the arguments from the main command")] public async Task jupyter_returns_error_if_connection_file_path_is_not_passed() { var testConsole = new TestConsole(); diff --git a/MLS.Agent.Tests/FactDependsOnJupyterNotOnPathAttribute.cs b/MLS.Agent.Tests/FactDependsOnJupyterNotOnPathAttribute.cs new file mode 100644 index 000000000..21c68961e --- /dev/null +++ b/MLS.Agent.Tests/FactDependsOnJupyterNotOnPathAttribute.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using System.IO; +using WorkspaceServer; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace MLS.Agent.Tests +{ + [XunitTestCaseDiscoverer("MLS.Agent.Tests.JupyterNotInstalledTestCaseDiscover", "MLS.Agent.Tests")] + public class FactDependsOnJupyterNotOnPathAttribute : FactAttribute + { + } + + public class JupyterNotInstalledTestCaseDiscover : IXunitTestCaseDiscoverer + { + private readonly IMessageSink messageSink; + + public JupyterNotInstalledTestCaseDiscover(IMessageSink messageSink) + { + this.messageSink = messageSink; + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + if (testMethod.TestClass.Class.Name.Contains("Integration") && FileSystemJupyterKernelSpec.CheckIfJupyterKernelSpecExists()) + { + yield break; + } + + yield return new XunitTestCase( + messageSink, + TestMethodDisplay.ClassAndMethod, + new TestMethodDisplayOptions(), + testMethod + ); + } + } +} \ No newline at end of file diff --git a/MLS.Agent.Tests/FactDependsOnJupyterOnPathAttribute.cs b/MLS.Agent.Tests/FactDependsOnJupyterOnPathAttribute.cs new file mode 100644 index 000000000..1c7cc6ab2 --- /dev/null +++ b/MLS.Agent.Tests/FactDependsOnJupyterOnPathAttribute.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using System.IO; +using WorkspaceServer; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace MLS.Agent.Tests +{ + [XunitTestCaseDiscoverer("MLS.Agent.Tests.JupyterInstalledTestCaseDiscoverer", "MLS.Agent.Tests")] + public class FactDependsOnJupyterOnPathAttribute : FactAttribute + { + } + + public class JupyterInstalledTestCaseDiscoverer : IXunitTestCaseDiscoverer + { + private readonly IMessageSink messageSink; + + public JupyterInstalledTestCaseDiscoverer(IMessageSink messageSink) + { + this.messageSink = messageSink; + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + if (testMethod.TestClass.Class.Name.Contains("Integration") && !FileSystemJupyterKernelSpec.CheckIfJupyterKernelSpecExists()) + { + yield break; + } + + yield return new XunitTestCase( + messageSink, + TestMethodDisplay.ClassAndMethod, + new TestMethodDisplayOptions(), + testMethod + ); + } + } +} \ No newline at end of file diff --git a/MLS.Agent.Tests/InMemoryJupyterKernelSpec.cs b/MLS.Agent.Tests/InMemoryJupyterKernelSpec.cs new file mode 100644 index 000000000..67ccadebe --- /dev/null +++ b/MLS.Agent.Tests/InMemoryJupyterKernelSpec.cs @@ -0,0 +1,39 @@ +// 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 MLS.Agent.Tools; +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace MLS.Agent.Tests +{ + public class InMemoryJupyterKernelSpec : IJupyterKernelSpec + { + private bool _successfulInstall; + + public InMemoryJupyterKernelSpec(bool successfulInstall) + { + _successfulInstall = successfulInstall; + } + + public Task ExecuteCommand(string command, string args = "") + { + throw new NotImplementedException(); + } + + public async Task InstallKernel(DirectoryInfo sourceDirectory) + { + if(_successfulInstall) + { + var installPath = Path.Combine(Directory.GetCurrentDirectory(), sourceDirectory.Name.ToLower()); + return new CommandLineResult(0, error: new List { $"[InstallKernelSpec] Installed kernelspec {sourceDirectory.Name} in {installPath}" }); + } + + return new CommandLineResult(1); + } + } +} \ No newline at end of file diff --git a/MLS.Agent.Tests/JupyterCommandLineTests.cs b/MLS.Agent.Tests/JupyterCommandLineTests.cs new file mode 100644 index 000000000..ad4637460 --- /dev/null +++ b/MLS.Agent.Tests/JupyterCommandLineTests.cs @@ -0,0 +1,32 @@ +// 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.CommandLine; +using System.Threading.Tasks; +using Xunit; + +namespace MLS.Agent.Tests +{ + public class JupyterCommandLineTests + { + [Fact] + public async Task Returns_error_when_jupyter_paths_could_not_be_obtained() + { + var console = new TestConsole(); + var jupyterCommandLine = new JupyterCommandLine(console, new InMemoryJupyterKernelSpec(false)); + await jupyterCommandLine.InvokeAsync(); + console.Error.ToString().Should().Contain(".NET kernel installation failed"); + } + + [Fact] + public async Task Prints_to_console_when_kernel_installation_succeded() + { + var console = new TestConsole(); + var jupyterCommandLine = new JupyterCommandLine(console, new InMemoryJupyterKernelSpec(true)); + await jupyterCommandLine.InvokeAsync(); + console.Out.ToString().Should().MatchEquivalentOf($"*[InstallKernelSpec] Installed kernelspec .net in *.net *"); + console.Out.ToString().Should().Contain(".NET kernel installation succeded"); + } + } +} \ No newline at end of file diff --git a/MLS.Agent.Tests/JupyterKernelSpecTests.cs b/MLS.Agent.Tests/JupyterKernelSpecTests.cs new file mode 100644 index 000000000..004fd780f --- /dev/null +++ b/MLS.Agent.Tests/JupyterKernelSpecTests.cs @@ -0,0 +1,81 @@ +// 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; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using WorkspaceServer.Tests; +using Xunit; + +namespace MLS.Agent.Tests +{ + public abstract class JupyterKernelSpecTests: IAsyncDisposable + { + protected List _installedKernels; + + public JupyterKernelSpecTests() + { + _installedKernels = new List(); + } + + public abstract IJupyterKernelSpec GetJupyterKernelSpec(bool success); + + [FactDependsOnJupyterOnPath] + public async Task Returns_sucess_output_when_kernel_installation_succeded() + { + //For the FileSystemJupyterKernelSpec, this fact needs jupyter to be on the path + //To run this test for FileSystemJupyterKernelSpec open Visual Studio inside anaconda prompt or in a terminal with + //path containing the environment variables for jupyter + + var kernelSpec = GetJupyterKernelSpec(true); + var kernelDir = Create.EmptyWorkspace().Directory; + + var result = await kernelSpec.InstallKernel(kernelDir); + result.ExitCode.Should().Be(0); + _installedKernels.Add(kernelDir.Name.ToLower()); + + //The actual jupyter instance is returning the output in the error field + result.Error.First().Should().MatchEquivalentOf($"[InstallKernelSpec] Installed kernelspec {kernelDir.Name} in *{kernelDir.Name}"); + } + + [FactDependsOnJupyterNotOnPath] + public async Task Returns_failure_when_kernel_installation_did_not_succeed() + { + var kernelSpec = GetJupyterKernelSpec(false); + var kernelDir = Create.EmptyWorkspace().Directory; + + var result = await kernelSpec.InstallKernel(kernelDir); + result.ExitCode.Should().Be(1); + } + + + public async ValueTask DisposeAsync() + { + var kernelSpec = GetJupyterKernelSpec(true); + foreach (var kernel in _installedKernels) + { + await kernelSpec.ExecuteCommand("uninstall", kernel); + } + } + } + + public class FileSystemJupyterKernelSpecIntegrationTests : JupyterKernelSpecTests + { + + public override IJupyterKernelSpec GetJupyterKernelSpec(bool success) + { + return new FileSystemJupyterKernelSpec(); + } + } + + public class InMemoryJupyterKernelSpecTests : JupyterKernelSpecTests + { + public override IJupyterKernelSpec GetJupyterKernelSpec(bool success) + { + return new InMemoryJupyterKernelSpec(success); + } + } +} \ No newline at end of file diff --git a/MLS.Agent/CommandLine/CommandLineParser.cs b/MLS.Agent/CommandLine/CommandLineParser.cs index 27e84a1c8..e6be9b71d 100644 --- a/MLS.Agent/CommandLine/CommandLineParser.cs +++ b/MLS.Agent/CommandLine/CommandLineParser.cs @@ -360,7 +360,8 @@ Command Jupyter() }; var connectionFileArgument = new Argument { - Name = "ConnectionFile" + Name = "ConnectionFile", + Arity = ArgumentArity.ZeroOrOne //should be removed once the commandlineapi allows subcommands to not have arguments from the main command }.ExistingOnly(); jupyterCommand.AddArgument(connectionFileArgument); @@ -392,6 +393,14 @@ Command Jupyter() return jupyter(options, console, startServer, context); }); + var installCommand = new Command("install", "Install the .NET kernel for Jupyter"); + installCommand.Handler = CommandHandler.Create((console) => + { + return new JupyterCommandLine(console, new FileSystemJupyterKernelSpec()).InvokeAsync(); + }); + + jupyterCommand.AddCommand(installCommand); + return jupyterCommand; } diff --git a/MLS.Agent/FileSystemJupyterKernelSpec.cs b/MLS.Agent/FileSystemJupyterKernelSpec.cs new file mode 100644 index 000000000..fc7e0bd9d --- /dev/null +++ b/MLS.Agent/FileSystemJupyterKernelSpec.cs @@ -0,0 +1,44 @@ +// 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 MLS.Agent.Tools; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Clockwise; + +namespace MLS.Agent +{ + public class FileSystemJupyterKernelSpec : IJupyterKernelSpec + { + public async Task ExecuteCommand(string command, string args = "") + { + if (!CheckIfJupyterKernelSpecExists()) + { + return new CommandLineResult(1, new List() { "Could not find jupyter kernelspec module" }); + } + + return await Tools.CommandLine.Execute("jupyter", $"kernelspec {command} {args}"); + } + + public Task InstallKernel(DirectoryInfo sourceDirectory) + { + return ExecuteCommand($"install {sourceDirectory.FullName}", "--user"); + } + + public static bool CheckIfJupyterKernelSpecExists() + { + var command = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "where" : "which"; + bool jupyterKernelSpecExists = false ; + + Task.Run(async ()=> { + var result = await Tools.CommandLine.Execute(command, "jupyter-kernelspec"); + jupyterKernelSpecExists = result.ExitCode == 0; + }).Wait(2000); + + return jupyterKernelSpecExists; + } + } +} diff --git a/MLS.Agent/IJupyterKernelSpec.cs b/MLS.Agent/IJupyterKernelSpec.cs new file mode 100644 index 000000000..a25f92450 --- /dev/null +++ b/MLS.Agent/IJupyterKernelSpec.cs @@ -0,0 +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 MLS.Agent.Tools; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace MLS.Agent +{ + public interface IJupyterKernelSpec + { + Task ExecuteCommand(string command, string args=""); + Task InstallKernel(DirectoryInfo sourceDirectory); + } +} \ No newline at end of file diff --git a/MLS.Agent/JupyterCommandLine.cs b/MLS.Agent/JupyterCommandLine.cs new file mode 100644 index 000000000..3d49fb94d --- /dev/null +++ b/MLS.Agent/JupyterCommandLine.cs @@ -0,0 +1,65 @@ +// 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 MLS.Agent.CommandLine; +using MLS.Agent.Tools; +using System; +using System.Collections; +using System.Collections.Generic; +using System.CommandLine; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using WorkspaceServer; + +namespace MLS.Agent +{ + public class JupyterCommandLine + { + private readonly IConsole _console; + private readonly IJupyterKernelSpec _jupyterKernelSpec; + + public JupyterCommandLine(IConsole console, IJupyterKernelSpec jupyterKernelSpec) + { + _console = console; + _jupyterKernelSpec = jupyterKernelSpec; + } + + public async Task InvokeAsync() + { + using (var disposableDirectory = DisposableDirectory.Create()) + { + var assembly = typeof(Program).Assembly; + + using (var resourceStream = assembly.GetManifestResourceStream("dotnetKernel.zip")) + { + var zipPath = Path.Combine(disposableDirectory.Directory.FullName, "dotnetKernel.zip"); + + using (var fileStream = new FileStream(zipPath, FileMode.Create, FileAccess.Write)) + { + resourceStream.CopyTo(fileStream); + } + + var dotnetDirectory = disposableDirectory.Directory.CreateSubdirectory(".NET"); + ZipFile.ExtractToDirectory(zipPath, dotnetDirectory.FullName); + + var result = await _jupyterKernelSpec.InstallKernel(dotnetDirectory); + if (result.ExitCode == 0) + { + _console.Out.WriteLine(string.Join('\n', result.Output)); + _console.Out.WriteLine(string.Join('\n', result.Error)); + _console.Out.WriteLine(".NET kernel installation succeded"); + return 0; + } + else + { + _console.Error.WriteLine($".NET kernel installation failed with error: {string.Join('\n', result.Error)}"); + return -1; + } + } + } + } + } +} \ No newline at end of file diff --git a/MLS.Agent/MLS.Agent.csproj b/MLS.Agent/MLS.Agent.csproj index 97f71d6f7..05cd9109d 100644 --- a/MLS.Agent/MLS.Agent.csproj +++ b/MLS.Agent/MLS.Agent.csproj @@ -32,7 +32,7 @@ - + all @@ -121,6 +121,9 @@ demo.zip + + dotnetKernel.zip + @@ -190,7 +193,7 @@ - + PreserveNewest @@ -209,4 +212,16 @@ + + + + + + + + + + + + diff --git a/MLS.Agent/MLS.Agent.v3.ncrunchproject b/MLS.Agent/MLS.Agent.v3.ncrunchproject index bec9489c9..a9a398b55 100644 --- a/MLS.Agent/MLS.Agent.v3.ncrunchproject +++ b/MLS.Agent/MLS.Agent.v3.ncrunchproject @@ -3,6 +3,7 @@ ..\docs\**.* wwwroot\**.* + ..\Microsoft.DotNet.Interactive.Jupyter\ContentFiles\**.* \ No newline at end of file