From 2f8362170c3a838b58b57c5c2e4c931bb85fe5f8 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Tue, 9 Feb 2021 10:37:10 -0800 Subject: [PATCH] load doc comments from xml files when available --- .../CSharpKernel.cs | 14 +- .../CachingMetadataResolver.cs | 20 ++- .../CompletionExtensions.cs | 8 +- .../LanguageServices/CompletionTests.cs | 127 +++++++++++++++- .../LanguageServices/HoverTextTests.cs | 136 +++++++++++++++++- .../LanguageServices/SignatureHelpTests.cs | 77 ++++++++++ .../Utility/TemporaryDirectory.cs | 34 +++++ .../Utility/TestAssemblyReference.cs | 52 +++++++ 8 files changed, 452 insertions(+), 16 deletions(-) create mode 100644 src/Microsoft.DotNet.Interactive.Tests/Utility/TemporaryDirectory.cs create mode 100644 src/Microsoft.DotNet.Interactive.Tests/Utility/TestAssemblyReference.cs diff --git a/src/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs b/src/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs index 8a34216476..edbacb02e2 100644 --- a/src/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs +++ b/src/Microsoft.DotNet.Interactive.CSharp/CSharpKernel.cs @@ -144,7 +144,7 @@ public override async Task SetVariableAsync(string name, object value, Type decl public async Task HandleAsync(RequestHoverText command, KernelInvocationContext context) { using var _ = new GCPressure(1024 * 1024); - + var document = _workspace.UpdateWorkingDocument(command.Code); var text = await document.GetTextAsync(); var cursorPosition = text.Lines.GetPosition(command.LinePosition.ToCodeAnalysisLinePosition()); @@ -352,7 +352,14 @@ private async Task> GetCompletionList( return Enumerable.Empty(); } - var items = completionList.Items.Select(item => item.ToModel()).ToArray(); + var items = new List(); + foreach (var item in completionList.Items) + { + var description = await service.GetDescriptionAsync(document, item); + var completionItem = item.ToModel(description); + items.Add(completionItem); + } + return items; } @@ -407,7 +414,8 @@ void ISupportNuget.RegisterResolvedPackageReferences(IReadOnlyList r.AssemblyPaths) - .Select(r => MetadataReference.CreateFromFile(r)); + .Select(r => CachingMetadataResolver.ResolveReferenceWithXmlDocumentationProvider(r)) + .ToArray(); ScriptOptions = ScriptOptions.AddReferences(references); } diff --git a/src/Microsoft.DotNet.Interactive.CSharp/CachingMetadataResolver.cs b/src/Microsoft.DotNet.Interactive.CSharp/CachingMetadataResolver.cs index 23d29a0388..3b9000d486 100644 --- a/src/Microsoft.DotNet.Interactive.CSharp/CachingMetadataResolver.cs +++ b/src/Microsoft.DotNet.Interactive.CSharp/CachingMetadataResolver.cs @@ -1,15 +1,14 @@ // 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.Collections.Concurrent; using System.Collections.Immutable; - +using System.IO; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Scripting; - namespace Microsoft.DotNet.Interactive.CSharp { internal sealed class CachingMetadataResolver : MetadataReferenceResolver, IEquatable @@ -26,7 +25,9 @@ private CachingMetadataResolver(ImmutableArray searchPaths, string baseD public override ImmutableArray ResolveReference(string reference, string baseFilePath, MetadataReferenceProperties properties) { - return _resolver.ResolveReference(reference, baseFilePath, properties); + var resolvedReferences = _resolver.ResolveReference(reference, baseFilePath, properties); + var xmlResolvedReferences = resolvedReferences.Select(r => ResolveReferenceWithXmlDocumentationProvider(r.FilePath, properties)).ToImmutableArray(); + return xmlResolvedReferences; } public override bool ResolveMissingAssemblies => _resolver.ResolveMissingAssemblies; @@ -39,11 +40,11 @@ public override PortableExecutableReference ResolveMissingAssembly(MetadataRefer public CachingMetadataResolver WithBaseDirectory(string baseDirectory) { - if (BaseDirectory == baseDirectory) { return this; } + return new CachingMetadataResolver(SearchPaths, baseDirectory); } @@ -51,11 +52,16 @@ public CachingMetadataResolver WithBaseDirectory(string baseDirectory) public string BaseDirectory => _resolver.BaseDirectory; + internal static PortableExecutableReference ResolveReferenceWithXmlDocumentationProvider(string path, MetadataReferenceProperties properties = default(MetadataReferenceProperties)) + { + var peReference = MetadataReference.CreateFromFile(path, properties, XmlDocumentationProvider.CreateFromFile(Path.ChangeExtension(path, ".xml"))); + return peReference; + } + public bool Equals(ScriptMetadataResolver other) => _resolver.Equals(other); public override bool Equals(object other) => Equals(other as ScriptMetadataResolver); public override int GetHashCode() => _resolver.GetHashCode(); - } -} \ No newline at end of file +} diff --git a/src/Microsoft.DotNet.Interactive.CSharp/CompletionExtensions.cs b/src/Microsoft.DotNet.Interactive.CSharp/CompletionExtensions.cs index b1988fca95..43c749e1c7 100644 --- a/src/Microsoft.DotNet.Interactive.CSharp/CompletionExtensions.cs +++ b/src/Microsoft.DotNet.Interactive.CSharp/CompletionExtensions.cs @@ -1,11 +1,10 @@ // 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.Collections.Immutable; -using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Tags; using Microsoft.DotNet.Interactive.Events; +using RoslynCompletionDescription = Microsoft.CodeAnalysis.Completion.CompletionDescription; using RoslynCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem; namespace Microsoft.DotNet.Interactive.CSharp @@ -50,14 +49,15 @@ public static string GetKind(this RoslynCompletionItem completionItem) return null; } - public static CompletionItem ToModel(this RoslynCompletionItem item) + public static CompletionItem ToModel(this RoslynCompletionItem item, RoslynCompletionDescription description) { return new CompletionItem( displayText: item.DisplayText, kind: item.GetKind(), filterText: item.FilterText, sortText: item.SortText, - insertText: item.FilterText); + insertText: item.FilterText, + documentation: description.Text); } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/CompletionTests.cs b/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/CompletionTests.cs index 03a2d17c33..3dfc760e41 100644 --- a/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/CompletionTests.cs +++ b/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/CompletionTests.cs @@ -316,5 +316,130 @@ public async Task magic_command_completion_commands_and_events_have_offsets_norm .Should() .Be(new LinePositionSpan(new LinePosition(line, 0), new LinePosition(line, 3))); } + + [Theory] + [InlineData(Language.CSharp, "/// Adds two numbers.\nint Add(int a, int b) => a + b;", "Ad$$", "Adds two numbers.")] + [InlineData(Language.FSharp, "/// Adds two numbers.\nlet add a b = a + b", "ad$$", "Adds two numbers.")] + public async Task completion_doc_comments_can_be_loaded_from_source_in_a_previous_submission(Language language, string previousSubmission, string markupCode, string expectedCompletionSubString) + { + using var kernel = CreateKernel(language); + + await SubmitCode(kernel, previousSubmission); + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Completions + .Should() + .ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains(expectedCompletionSubString)); + } + + [Theory] + [InlineData(Language.CSharp)] + [InlineData(Language.FSharp)] + public async Task completion_contains_doc_comments_from_individually_referenced_assemblies_with_xml_files(Language language) + { + using var assembly = new TestAssemblyReference("Project", "netstandard2.0", "Program.cs", @" +public class C +{ + /// This is the answer. + public static int TheAnswer => 42; +} +"); + var assemblyPath = await assembly.BuildAndGetPathToAssembly(); + + var assemblyReferencePath = language switch + { + Language.CSharp => assemblyPath, + Language.FSharp => assemblyPath.Replace("\\", "\\\\") + }; + + using var kernel = CreateKernel(language); + + await SubmitCode(kernel, $"#r \"{assemblyReferencePath}\""); + + var markupCode = "C.TheAns$$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Completions + .Should() + .ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("This is the answer.")); + } + + [Fact] + public async Task csharp_completions_can_read_doc_comments_from_nuget_packages_after_forcing_the_assembly_to_load() + { + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + // The following line forces the assembly and the doc comments to be loaded + await SubmitCode(kernel, "var _unused = Newtonsoft.Json.JsonConvert.Null;"); + + var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Completions + .Should() + .ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("Represents JavaScript's null as a string. This field is read-only.")); + } + + [Fact(Skip = "https://github.com/dotnet/interactive/issues/1071 N.b., the preceeding test can be deleted when this one is fixed.")] + public async Task csharp_completions_can_read_doc_comments_from_nuget_packages() + { + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Completions + .Should() + .ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("Represents JavaScript's null as a string. This field is read-only.")); + } + + [Fact] + public async Task fsharp_completions_can_read_doc_comments_from_nuget_packages() + { + using var kernel = CreateKernel(Language.FSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestCompletions(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Completions + .Should() + .ContainSingle(ci => ci.Documentation != null && ci.Documentation.Contains("Represents JavaScript's null as a string. This field is read-only.")); + } } -} \ No newline at end of file +} diff --git a/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/HoverTextTests.cs b/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/HoverTextTests.cs index 040eb67aae..7983c2efeb 100644 --- a/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/HoverTextTests.cs +++ b/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/HoverTextTests.cs @@ -9,6 +9,7 @@ using Xunit; using Xunit.Abstractions; +#pragma warning disable 8509 namespace Microsoft.DotNet.Interactive.Tests.LanguageServices { public class HoverTextTests : LanguageKernelTestBase @@ -116,6 +117,139 @@ public async Task language_service_methods_run_deferred_commands(Language langua .ContainEquivalentOf(new FormattedValue(expectedMimeType, expectedContent)); } + [Theory] + [InlineData(Language.CSharp, "/// Adds two numbers.\nint Add(int a, int b) => a + b;", "Ad$$d(1, 2)", "Adds two numbers.")] + [InlineData(Language.FSharp, "/// Adds two numbers.\nlet add a b = a + b", "ad$$d 1 2", "Adds two numbers.")] + public async Task hover_text_doc_comments_can_be_loaded_from_source_in_a_previous_submission(Language language, string previousSubmission, string markupCode, string expectedHoverTextSubString) + { + using var kernel = CreateKernel(language); + + await SubmitCode(kernel, previousSubmission); + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + var commandResult = await SendHoverRequest(kernel, code, line, character); + + var events = commandResult.KernelEvents.ToSubscribedList(); + + events + .Should() + .ContainSingle() + .Which + .Content + .Should() + .ContainSingle(fv => fv.Value.Contains(expectedHoverTextSubString)); + } + + [Theory] + [InlineData(Language.CSharp, "var s = new Sample$$Class();")] + [InlineData(Language.FSharp, "let s = Sample$$Class()")] + public async Task hover_text_can_read_doc_comments_from_individually_referenced_assemblies_with_xml_files(Language language, string markupCode) + { + using var assembly = new TestAssemblyReference("Project", "netstandard2.0", "Program.cs", @" +public class SampleClass +{ + /// A sample class constructor. + public SampleClass() { } +} +"); + var assemblyPath = await assembly.BuildAndGetPathToAssembly(); + + var assemblyReferencePath = language switch + { + Language.CSharp => assemblyPath, + Language.FSharp => assemblyPath.Replace("\\", "\\\\") + }; + + using var kernel = CreateKernel(language); + + await SubmitCode(kernel, $"#r \"{assemblyReferencePath}\""); + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + var commandResult = await SendHoverRequest(kernel, code, line, character); + + var events = commandResult.KernelEvents.ToSubscribedList(); + + events + .Should() + .ContainSingle() + .Which + .Content + .Should() + .ContainSingle(fv => fv.Value.Contains("A sample class constructor.")); + } + + [Fact] + public async Task csharp_hover_text_can_read_doc_comments_from_nuget_packages_after_forcing_the_assembly_to_load() + { + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + // The following line forces the assembly and the doc comments to be loaded + await SubmitCode(kernel, "var _unused = Newtonsoft.Json.JsonConvert.Null;"); + + var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$ll"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + var commandResult = await SendHoverRequest(kernel, code, line, character); + + var events = commandResult.KernelEvents.ToSubscribedList(); + + events + .Should() + .ContainSingle() + .Which + .Content + .Should() + .ContainSingle(fv => fv.Value.Contains("Represents JavaScript's null as a string. This field is read-only.")); + } + + [Fact(Skip = "https://github.com/dotnet/interactive/issues/1071 N.b., the preceeding test can be deleted when this one is fixed.")] + public async Task csharp_hover_text_can_read_doc_comments_from_nuget_packages() + { + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$ll"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + var commandResult = await SendHoverRequest(kernel, code, line, character); + + var events = commandResult.KernelEvents.ToSubscribedList(); + + events + .Should() + .ContainSingle() + .Which + .Content + .Should() + .ContainSingle(fv => fv.Value.Contains("Represents JavaScript's null as a string. This field is read-only.")); + } + + [Fact] + public async Task fsharp_hover_text_can_read_doc_comments_from_nuget_packages() + { + using var kernel = CreateKernel(Language.FSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + var markupCode = "Newtonsoft.Json.JsonConvert.Nu$$ll"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + var commandResult = await SendHoverRequest(kernel, code, line, character); + + var events = commandResult.KernelEvents.ToSubscribedList(); + + events + .Should() + .ContainSingle() + .Which + .Content + .Should() + .ContainSingle(fv => fv.Value.Contains("Represents JavaScript's `null` as a string. This field is read-only.")); + } + [Theory] [InlineData(Language.CSharp, "Console.Write$$Line();", "text/markdown", "void Console.WriteLine() (+ 17 overloads)")] [InlineData(Language.FSharp, "ex$$it 0", "text/markdown", "```fsharp\nval exit: \n exitcode: int \n -> 'T\n```\n\n----\nExit the current hardware isolated process, if security settings permit,\n otherwise raise an exception. Calls `System.Environment.Exit`.\n\n`exitcode`: The exit code to use.\n\n**Generic parameters**\n\n* `'T` is `obj`\n\n----\n*Full name: Microsoft.FSharp.Core.Operators.exit*\n\n----\n*Assembly: FSharp.Core*")] @@ -242,4 +376,4 @@ public async Task csharp_hover_text_is_returned_for_shadowing_variables(Language .ContainSingle(fv => fv.Value == expected); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/SignatureHelpTests.cs b/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/SignatureHelpTests.cs index c37280e44b..b391b3cf7f 100644 --- a/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/SignatureHelpTests.cs +++ b/src/Microsoft.DotNet.Interactive.Tests/LanguageServices/SignatureHelpTests.cs @@ -9,6 +9,7 @@ using Xunit; using Xunit.Abstractions; +#pragma warning disable 8509 namespace Microsoft.DotNet.Interactive.Tests.LanguageServices { public class SignatureHelpTests : LanguageKernelTestBase @@ -115,5 +116,81 @@ public async Task signature_help_can_return_doc_comments_from_source(Language la .Should() .Be(expectedMethodDocumentation); } + + [Fact] + public async Task csharp_signature_help_contains_doc_comments_from_individually_referenced_assemblies_with_xml_files() + { + using var assembly = new TestAssemblyReference("Project", "netstandard2.0", "Program.cs", @" +public class SampleClass +{ + /// A sample class constructor. + public SampleClass() { } +} +"); + var assemblyPath = await assembly.BuildAndGetPathToAssembly(); + + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, $"#r \"{assemblyPath}\""); + + var markupCode = "new SampleClass($$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var column); + + await kernel.SendAsync(new RequestSignatureHelp(code, new LinePosition(line, column))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Signatures + .Should() + .ContainSingle(sh => sh.Documentation.Value.Contains("A sample class constructor.")); + } + + [Fact] + public async Task csharp_signature_hep_can_read_doc_comments_from_nuget_packages_after_forcing_the_assembly_to_load() + { + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + // The following line forces the assembly and the doc comments to be loaded + await SubmitCode(kernel, "var _unused = Newtonsoft.Json.JsonConvert.Null;"); + + var markupCode = "Newtonsoft.Json.JsonConvert.DeserializeObject($$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestSignatureHelp(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Signatures + .Should() + .ContainSingle(sh => sh.Documentation.Value.Contains("Deserializes the JSON to a .NET object.")); + } + + [Fact(Skip = "https://github.com/dotnet/interactive/issues/1071 N.b., the preceeding test can be deleted when this one is fixed.")] + public async Task csharp_signature_hep_can_read_doc_comments_from_nuget_packages() + { + using var kernel = CreateKernel(Language.CSharp); + + await SubmitCode(kernel, "#r \"nuget: Newtonsoft.Json, 12.0.3\""); + + var markupCode = "Newtonsoft.Json.JsonConvert.DeserializeObject($$"; + + MarkupTestFile.GetLineAndColumn(markupCode, out var code, out var line, out var character); + await kernel.SendAsync(new RequestSignatureHelp(code, new LinePosition(line, character))); + + KernelEvents + .Should() + .ContainSingle() + .Which + .Signatures + .Should() + .ContainSingle(sh => sh.Documentation.Value.Contains("Deserializes the JSON to a .NET object.")); + } } } diff --git a/src/Microsoft.DotNet.Interactive.Tests/Utility/TemporaryDirectory.cs b/src/Microsoft.DotNet.Interactive.Tests/Utility/TemporaryDirectory.cs new file mode 100644 index 0000000000..9a7b6c179d --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.Tests/Utility/TemporaryDirectory.cs @@ -0,0 +1,34 @@ +// 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.IO; + +namespace Microsoft.DotNet.Interactive.Tests.Utility +{ + public class TemporaryDirectory : IDisposable + { + public DirectoryInfo Directory { get; } + + public TemporaryDirectory(params (string relativePath, string content)[] contents) + { + var tempPath = Path.GetTempPath(); + var dirName = Guid.NewGuid().ToString(); + var fullPath = Path.Combine(tempPath, dirName); + var parentDirectory = new DirectoryInfo(fullPath); + Directory = DirectoryUtility.CreateDirectory(parentDirectory: parentDirectory); + DirectoryUtility.Populate(Directory, contents); + } + + public void Dispose() + { + try + { + Directory.Delete(true); + } + catch + { + } + } + } +} diff --git a/src/Microsoft.DotNet.Interactive.Tests/Utility/TestAssemblyReference.cs b/src/Microsoft.DotNet.Interactive.Tests/Utility/TestAssemblyReference.cs new file mode 100644 index 0000000000..1f663022bb --- /dev/null +++ b/src/Microsoft.DotNet.Interactive.Tests/Utility/TestAssemblyReference.cs @@ -0,0 +1,52 @@ +// 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.IO; +using System.Threading.Tasks; +using Microsoft.DotNet.Interactive.Utility; + +namespace Microsoft.DotNet.Interactive.Tests.Utility +{ + public class TestAssemblyReference : IDisposable + { + public string ProjectName { get; } + public string TargetFramework { get; } + public TemporaryDirectory Directory { get; } + + public TestAssemblyReference(string projectName, string targetFramework, string sourceFileName, string sourceFileContents) + { + ProjectName = projectName; + TargetFramework = targetFramework; + Directory = new TemporaryDirectory( + ($"{ProjectName}.csproj", $@" + + + {TargetFramework} + true + +"), + (sourceFileName, sourceFileContents) + ); + } + + public async Task BuildAndGetPathToAssembly() + { + var dotnet = new Dotnet(Directory.Directory); + var result = await dotnet.Build(); + result.ThrowOnFailure("Failed to build sample assembly"); + var assemblyPath = Path.Combine(Directory.Directory.FullName, "bin", "Debug", TargetFramework, $"{ProjectName}.dll"); + if (!File.Exists(assemblyPath)) + { + throw new Exception($"The expected assembly was not found at path '{assemblyPath}'."); + } + + return assemblyPath; + } + + public void Dispose() + { + Directory.Dispose(); + } + } +}