diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..acdab74f7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ + +[*.{c,c++,cc,cp,cpp,cu,cuh,cxx,h,hh,hpp,hxx,inc,inl,ino,ipp,mpp,proto,tpp}] +indent_style=tab +indent_size=tab +tab_width=4 + +[*.{asax,ascx,aspx,cs,cshtml,css,htm,html,js,jsx,master,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] +indent_style=space +indent_size=4 +tab_width=4 + +[*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] +indent_style=space +indent_size=2 +tab_width=2 + +[*] + +# Microsoft .NET properties +csharp_new_line_before_members_in_object_initializers=false +csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion +csharp_style_var_elsewhere=true:hint +csharp_style_var_for_built_in_types=true:hint +csharp_style_var_when_type_is_apparent=true:hint +dotnet_style_predefined_type_for_locals_parameters_members=true:hint +dotnet_style_predefined_type_for_member_access=true:hint +dotnet_style_qualification_for_event=false:warning +dotnet_style_qualification_for_field=false:warning +dotnet_style_qualification_for_method=false:warning +dotnet_style_qualification_for_property=false:warning +dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint diff --git a/.gitignore b/.gitignore index 1c4634087..a0336fe33 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ *.user *.userosscache *.sln.docstates -*NCrunch* .trydotnet-* # User-specific files (MonoDevelop/Xamarin Studio) diff --git a/DotNetTry.sln.DotSettings b/DotNetTry.sln.DotSettings new file mode 100644 index 000000000..7fa67efcf --- /dev/null +++ b/DotNetTry.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/DotNetTry.v3.ncrunchsolution b/DotNetTry.v3.ncrunchsolution new file mode 100644 index 000000000..10420ac91 --- /dev/null +++ b/DotNetTry.v3.ncrunchsolution @@ -0,0 +1,6 @@ + + + True + True + + \ No newline at end of file diff --git a/MLS.Agent.Tests/ApiContracts/ApiOutputContractTests.cs b/MLS.Agent.Tests/ApiContracts/ApiOutputContractTests.cs index c096a9329..a1092994f 100644 --- a/MLS.Agent.Tests/ApiContracts/ApiOutputContractTests.cs +++ b/MLS.Agent.Tests/ApiContracts/ApiOutputContractTests.cs @@ -9,10 +9,8 @@ using Microsoft.DotNet.Try.Protocol; using Microsoft.DotNet.Try.Protocol.Tests; using Recipes; -using WorkspaceServer.Tests; using Xunit; using Xunit.Abstractions; -using Package = WorkspaceServer.Packaging.Package; namespace MLS.Agent.Tests.ApiContracts { @@ -31,12 +29,11 @@ public ApiOutputContractTests(ITestOutputHelper output) : base(output) [Fact] public async Task The_Run_contract_for_compiling_code_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var viewport = ViewportCode(); var requestJson = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode(), @@ -55,12 +52,11 @@ public async Task The_Run_contract_for_compiling_code_has_not_been_broken() [Fact] public async Task The_Run_contract_for_noncompiling_code_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var viewport = ViewportCode("doesn't compile"); var request = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode(), @@ -81,12 +77,11 @@ public async Task The_Run_contract_for_noncompiling_code_has_not_been_broken() [Fact] public async Task The_Compile_contract_for_compiling_code_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var viewport = ViewportCode(); var requestJson = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode(), @@ -116,12 +111,11 @@ public async Task The_Compile_contract_for_compiling_code_has_not_been_broken() [Fact] public async Task The_Compile_contract_for_noncompiling_code_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var viewport = ViewportCode("doesn't compile"); var request = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode(), @@ -142,12 +136,11 @@ public async Task The_Compile_contract_for_noncompiling_code_has_not_been_broken [Fact] public async Task The_Completions_contract_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var viewport = ViewportCode("Console.Ou$$"); var requestJson = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode(), @@ -166,12 +159,11 @@ public async Task The_Completions_contract_has_not_been_broken() [Fact] public async Task The_signature_help_contract_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var viewport = ViewportCode("Console.Write($$);"); var requestJson = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode(), @@ -190,10 +182,9 @@ public async Task The_signature_help_contract_has_not_been_broken() [Fact] public async Task The_instrumentation_contract_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var requestJson = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode("int a = 1; int b = 2; a = 3; b = a;") @@ -212,10 +203,9 @@ public async Task The_instrumentation_contract_has_not_been_broken() [Fact] public async Task The_run_contract_with_no_instrumentation_has_not_been_broken() { - var package = await Package.Copy(await Default.ConsoleWorkspace()); var requestJson = new WorkspaceRequest( new Workspace( - workspaceType: package.Name, + workspaceType: "console", buffers: new[] { EntrypointCode("int a = 1; int b = 2; a = 3; b = a;") @@ -248,7 +238,7 @@ public static void Main() }} }}".EnforceLF(); - MarkupTestFile.GetPosition(input, out string output, out var position); + MarkupTestFile.GetPosition(input, out var output, out var position); return new Buffer( "Program.cs", @@ -282,7 +272,7 @@ public static object Method() }} }}".EnforceLF(); - MarkupTestFile.GetPosition(input, out string output, out var position); + MarkupTestFile.GetPosition(input, out var output, out var position); return new Buffer( "ViewportCode.cs", diff --git a/MLS.Agent.Tests/ApiViaHttpTests.cs b/MLS.Agent.Tests/ApiViaHttpTests.cs index 42f1f097b..aa57fbbc5 100644 --- a/MLS.Agent.Tests/ApiViaHttpTests.cs +++ b/MLS.Agent.Tests/ApiViaHttpTests.cs @@ -27,6 +27,7 @@ using HtmlAgilityPack; using System.Web; using MLS.Agent.Controllers; +using WorkspaceServer.Tests.Packaging; using CodeManipulation = WorkspaceServer.Tests.CodeManipulation; using SourceFile = Microsoft.DotNet.Try.Protocol.ClientApi.SourceFile; @@ -82,8 +83,7 @@ public async Task The_compile_endpoint_returns_bad_request_if_workspace_type_is_ public async Task The_workspace_endpoint_compiles_code_using_dotnet_when_a_non_script_workspace_type_is_specified() { var output = Guid.NewGuid().ToString(); - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); - var requestJson = Create.SimpleWorkspaceRequestAsJson(output, package.Name); + var requestJson = Create.SimpleWorkspaceRequestAsJson(output, "console"); var response = await CallRun(requestJson); @@ -100,7 +100,7 @@ public async Task The_workspace_endpoint_compiles_code_using_dotnet_when_a_non_s public async Task The_workspace_endpoint_will_prevent_compiling_if_is_in_language_service_mode() { var output = Guid.NewGuid().ToString(); - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); + var package = await PackageUtilities.Copy(await Default.ConsoleWorkspace()); var requestJson = Create.SimpleWorkspaceRequestAsJson(output, package.Name); @@ -112,12 +112,12 @@ public async Task The_workspace_endpoint_will_prevent_compiling_if_is_in_languag [Fact] public async Task When_a_non_script_workspace_type_is_specified_then_code_fragments_cannot_be_compiled_successfully() { - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); + var requestJson = new WorkspaceRequest( Workspace.FromSource( @"Console.WriteLine(""hello!"");", - workspaceType: package.Name, + workspaceType: "console", id: "Program.cs")).ToJson(); var response = await CallRun(requestJson); @@ -201,8 +201,8 @@ public async Task Sending_payloads_that_cannot_be_deserialized_results_in_BadReq [Fact] public async Task A_script_snippet_workspace_can_be_used_to_get_completions() { - var (processed, position) = WorkspaceServer.Tests.CodeManipulation.ProcessMarkup("Console.$$"); - using (var agent = new AgentService()) + var (processed, position) = CodeManipulation.ProcessMarkup("Console.$$"); + using (var agent = new AgentService(StartupOptions.FromCommandLine("hosted"))) { var json = new WorkspaceRequest( requestId: "TestRun", @@ -346,7 +346,6 @@ public static IEnumerable Fibonacci() } }".EnforceLF(); #endregion - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); var (processed, position) = CodeManipulation.ProcessMarkup(generator); var log = new LogEntryList(); using (LogEvents.Subscribe(log.Add)) @@ -356,7 +355,7 @@ public static IEnumerable Fibonacci() new WorkspaceRequest(activeBufferId: "generators/FibonacciGenerator.cs", requestId: "TestRun", workspace: Workspace.FromSources( - package.Name, + "console", ("Program.cs", program, 0), ("generators/FibonacciGenerator.cs", processed, position) )).ToJson(); @@ -423,7 +422,6 @@ public static IEnumerable Fibonacci() }".EnforceLF(); #endregion - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); var (processed, position) = CodeManipulation.ProcessMarkup(generator); var log = new LogEntryList(); using (LogEvents.Subscribe(log.Add)) @@ -433,7 +431,7 @@ public static IEnumerable Fibonacci() new WorkspaceRequest(activeBufferId: "generators/FibonacciGenerator.cs", requestId: "TestRun", workspace: Workspace.FromSources( - package.Name, + "console", ("Program.cs", program, 0), ("generators/FibonacciGenerator.cs", processed, position) )).ToJson(); @@ -500,7 +498,7 @@ public static IEnumerable Fibonacci() }".EnforceLF(); #endregion - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace(), "a space"); + var package = await PackageUtilities.Copy(await Default.ConsoleWorkspace(), "a space"); var (processed, position) = CodeManipulation.ProcessMarkup(generator); var log = new LogEntryList(); using (LogEvents.Subscribe(log.Add)) @@ -578,7 +576,6 @@ public static IEnumerable Fibonacci() }".EnforceLF(); #endregion - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); var (processed, position) = CodeManipulation.ProcessMarkup(generator); var log = new LogEntryList(); using (LogEvents.Subscribe(log.Add)) @@ -588,7 +585,7 @@ public static IEnumerable Fibonacci() new WorkspaceRequest(activeBufferId: "generators/FibonacciGenerator.cs", requestId: "TestRun", workspace: Workspace.FromSources( - package.Name, + "console", ("Program.cs", program, 0), ("generators/FibonacciGenerator.cs", processed, position) )).ToJson(); @@ -613,14 +610,10 @@ public static IEnumerable Fibonacci() } } - [Fact] + [Fact(Skip = "WIP aspnet.webapi")] public async Task When_aspnet_webapi_workspace_request_succeeds_then_output_shows_web_response() { - var workspaceType = await Create.WebApiWorkspaceCopy(); - var workspace = WorkspaceFactory.CreateWorkspaceFromDirectory( - workspaceType.Directory, - workspaceType.Directory.Name); - + var workspace = new Workspace(workspaceType:"aspnet.webapi", buffers:new []{new Buffer("empty.cs", "")}); var request = new WorkspaceRequest(workspace, httpRequest: new HttpRequest("/api/values", "get"), requestId: "TestRun"); var json = request.ToJson(); @@ -648,10 +641,10 @@ public async Task When_aspnet_webapi_workspace_request_succeeds_then_output_show "]"); } - [Fact(Skip = "WIP")] + [Fact(Skip = "WIP aspnet.webapi")] public async Task When_aspnet_webapi_workspace_request_succeeds_then_standard_out_is_available_on_response() { - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.WebApiWorkspace()); + var package = await PackageUtilities.Copy(await Default.WebApiWorkspace()); await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(10.Minutes())); var workspace = WorkspaceFactory.CreateWorkspaceFromDirectory(package.Directory, package.Directory.Name); @@ -669,10 +662,10 @@ public async Task When_aspnet_webapi_workspace_request_succeeds_then_standard_ou throw new NotImplementedException(); } - [Fact] + [Fact(Skip = "WIP aspnet.webapi")] public async Task When_aspnet_webapi_workspace_request_fails_then_diagnostics_are_returned() { - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.WebApiWorkspace()); + var package = await PackageUtilities.Copy(await Default.WebApiWorkspace()); await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(10.Minutes())); var workspace = WorkspaceFactory.CreateWorkspaceFromDirectory(package.Directory, package.Directory.Name); var nonCompilingBuffer = new Buffer("broken.cs", "this does not compile", 0); @@ -696,9 +689,8 @@ public async Task When_aspnet_webapi_workspace_request_fails_then_diagnostics_ar public async Task When_Run_times_out_in_console_workspace_server_code_then_the_response_code_is_504() { var code = @"public class Program { public static void Main() { Console.WriteLine(); } }"; - var package = await WorkspaceServer.Packaging.Package.Copy(await Default.ConsoleWorkspace()); - var workspace = Workspace.FromSource(code.EnforceLF(), package.Name); + var workspace = Workspace.FromSource(code.EnforceLF(), "console"); var requestJson = new WorkspaceRequest(workspace).ToJson(); @@ -776,10 +768,19 @@ public async Task When_Run_times_out_in_user_code_then_the_response_code_is_417( { Clock.Reset(); - var workspace = - workspaceType == "script" - ? Workspace.FromSource(code, "script") - : Workspace.FromSource(code, (await Create.ConsoleWorkspaceCopy()).Name); + Workspace workspace = null; + if (workspaceType == "script") + { + workspace = Workspace.FromSource(code, "script"); + } + else + { + var package = Create.EmptyWorkspace(); + var build = await Create.NewPackage(package.Name, package.Directory, Create.ConsoleConfiguration); + workspace = Workspace.FromSource(code, build.Name); + } + + var requestJson = new WorkspaceRequest(workspace).ToJson(); var response = await CallRun(requestJson, 10000); @@ -874,12 +875,11 @@ public async Task Can_extract_regions_from_files() [Fact] public async Task Returns_200_if_the_package_exists() { - var package = await Create.ConsoleWorkspaceCopy(); var packageVersion = "1.0.0"; using(var agent = new AgentService()) { - var response = await agent.GetAsync($@"/packages/{package.Name}/{packageVersion}"); + var response = await agent.GetAsync($@"/packages/console/{packageVersion}"); response.StatusCode.Should().Be(HttpStatusCode.OK); } } @@ -900,12 +900,12 @@ public async Task Returns_404_if_the_package_does_not_exist() [Fact] public async Task Returns_blazor_false_if_the_package_does_not_contain_blazor_runner() { - var package = await Create.ConsoleWorkspaceCopy(); + var packageVersion = "1.0.0"; using (var agent = new AgentService()) { - var response = await agent.GetAsync($@"/packages/{package.Name}/{packageVersion}"); + var response = await agent.GetAsync($@"/packages/console/{packageVersion}"); response.Should().BeSuccessful(); var result = await response.Content.ReadAsStringAsync(); result.FromJsonTo() diff --git a/MLS.Agent.Tests/ApiViaHttpTestsBase.cs b/MLS.Agent.Tests/ApiViaHttpTestsBase.cs index 270c2e10a..e674a4dc5 100644 --- a/MLS.Agent.Tests/ApiViaHttpTestsBase.cs +++ b/MLS.Agent.Tests/ApiViaHttpTestsBase.cs @@ -63,7 +63,7 @@ private static async Task Call( StartupOptions options = null) { HttpResponseMessage response; - using (var agent = new AgentService(options)) + using (var agent = new AgentService(options ?? StartupOptions.FromCommandLine("hosted"))) { var request = new HttpRequestMessage( HttpMethod.Post, diff --git a/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs b/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs index 7d00a9fe3..879dd7bca 100644 --- a/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs +++ b/MLS.Agent.Tests/Markdown/CodeBlockAnnotationExtensionTests.cs @@ -63,7 +63,7 @@ static void MyProgram(string[] args) ("Program.cs", fileContent), ("sample.csproj", "") }; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var document = $@"```{language} --source-file Program.cs ```"; @@ -81,7 +81,7 @@ public async Task Does_not_insert_code_when_specified_language_is_not_csharp() var testDir = TestAssets.SampleConsole; var directoryAccessor = new InMemoryDirectoryAccessor(testDir); - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var document = @" ```js --source-file Program.cs console.log(""Hello World""); @@ -100,7 +100,7 @@ public async Task Does_not_insert_code_when_csharp_is_specified_but_no_additiona var testDir = TestAssets.SampleConsole; var directoryAccessor = new InMemoryDirectoryAccessor(testDir); - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var document = @" ```cs Console.WriteLine(""Hello World""); @@ -117,7 +117,7 @@ public async Task Error_messsage_is_displayed_when_the_linked_file_does_not_exis { ("sample.csproj", "") }; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var document = @"```cs --source-file DOESNOTEXIST ```"; @@ -133,7 +133,7 @@ public async Task Error_message_is_displayed_when_no_project_is_specified_and_no { ("Program.cs", "") }; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var document = @"```cs --source-file Program.cs ```"; @@ -155,7 +155,7 @@ public async Task Error_message_is_displayed_when_a_project_is_specified_but_the var document = $@"```cs --project {projectPath} --source-file Program.cs ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); html.Should().Contain($"Project not found: ./{projectPath}"); @@ -172,7 +172,7 @@ public async Task Sets_the_trydotnet_package_attribute_using_the_passed_project_ ("src/sample/sample.csproj", "") }; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var package = "../src/sample/sample.csproj"; var document = @@ -277,7 +277,7 @@ static void MyProgram(string[] args) var document = @"```cs --source-file Program.cs --region codeRegion ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); var htmlDocument = new HtmlDocument(); @@ -307,7 +307,7 @@ public async Task Sets_the_trydotnet_filename_using_the_filename_specified_in_th var document = $@"```cs --source-file {filename} --region codeRegion ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); var htmlDocument = new HtmlDocument(); @@ -337,7 +337,7 @@ public async Task Sets_the_trydotnet_filename_using_the_filename_specified_in_th var document = $@"```cs --source-file {sourceFile} --destination-file {destinationFile} --region codeRegion ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); var htmlDocument = new HtmlDocument(); @@ -366,7 +366,7 @@ public async Task Sets_the_trydotnet_region_using_the_region_passed_in_the_markd var document = $@"```cs --source-file Program.cs --region {region} ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); var htmlDocument = new HtmlDocument(); @@ -391,7 +391,7 @@ public async Task If_the_specified_region_does_not_exist_then_an_error_message_i var document = $@"```cs --source-file Program.cs --region {region} ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); var htmlDocument = new HtmlDocument(); @@ -422,7 +422,7 @@ public async Task If_the_specified_region_exists_more_than_once_then_an_error_is var document = $@"```cs --source-file Program.cs --region {region} ```"; - var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()).Build(); + var pipeline = new MarkdownPipelineBuilder().UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder).Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); var htmlDocument = new HtmlDocument(); @@ -446,7 +446,7 @@ public async Task Sets_the_trydotnet_session_using_the_session_passed_in_the_mar $@"```cs --source-file Program.cs --session {session} ```"; var pipeline = new MarkdownPipelineBuilder() - .UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()) + .UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder) .Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); @@ -473,7 +473,7 @@ public async Task Sets_the_trydotnet_session_to_a_default_value_when_a_session_i @"```cs --source-file Program.cs ```"; var pipeline = new MarkdownPipelineBuilder() - .UseCodeBlockAnnotations(directoryAccessor, PackageRegistry.CreateForHostedMode()) + .UseCodeBlockAnnotations(directoryAccessor, Default.PackageFinder) .Build(); var html = (await pipeline.RenderHtmlAsync(document)).EnforceLF(); diff --git a/MLS.Agent.Tests/Markdown/DirectoryAccessorTests.cs b/MLS.Agent.Tests/Markdown/DirectoryAccessorTests.cs index 5cf737c7d..128ff70e4 100644 --- a/MLS.Agent.Tests/Markdown/DirectoryAccessorTests.cs +++ b/MLS.Agent.Tests/Markdown/DirectoryAccessorTests.cs @@ -10,6 +10,7 @@ using WorkspaceServer; using WorkspaceServer.Packaging; using WorkspaceServer.Tests; +using WorkspaceServer.Tests.Packaging; using WorkspaceServer.Tests.TestUtility; using Xunit; using static Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; @@ -41,6 +42,25 @@ public void It_can_retrieve_all_files_recursively() .Contain(new RelativeFilePath("Subdirectory/Tutorial.md")); } + [Fact] + public void It_can_retrieve_all_files_at_root() + { + var directory = GetDirectory(TestAssets.SampleConsole); + + var files = directory.GetAllFiles(); + + files.Should() + .Contain(new RelativeFilePath("BasicConsoleApp.csproj")) + .And + .Contain(new RelativeFilePath("Program.cs")) + .And + .Contain(new RelativeFilePath("Readme.md")) + .And + .NotContain(new RelativeFilePath("Subdirectory/AnotherProgram.cs")) + .And + .NotContain(new RelativeFilePath("Subdirectory/Tutorial.md")); + } + [Fact] public void GetAllFilesRecursively_does_not_return_directories() { @@ -244,7 +264,7 @@ public class FileSystemDirectoryAccessorTests : DirectoryAccessorTests { public override IDirectoryAccessor CreateDirectory([CallerMemberName]string testName = null) { - var directory = Package.CreateDirectory(testName); + var directory = PackageUtilities.CreateDirectory(testName); return new FileSystemDirectoryAccessor(directory); } diff --git a/MLS.Agent.Tests/Markdown/MarkdownFileTests.cs b/MLS.Agent.Tests/Markdown/MarkdownFileTests.cs index ab21a9790..db3631d68 100644 --- a/MLS.Agent.Tests/Markdown/MarkdownFileTests.cs +++ b/MLS.Agent.Tests/Markdown/MarkdownFileTests.cs @@ -11,7 +11,6 @@ using MLS.Agent.CommandLine; using MLS.Agent.Markdown; using WorkspaceServer.Tests; -using WorkspaceServer; using WorkspaceServer.Tests.TestUtility; using Xunit; using Xunit.Abstractions; @@ -40,7 +39,7 @@ public async Task Renders_html_content_for_the_files_in_the_root_path() [Fact] public async Task Renders_html_content_for_files_in_subdirectories() { - var html = await RenderHtml(("Subdirectory/Tutorial.md", "This is a sample *tutorial file*")); + var html = await RenderHtml(("SubDirectory/Tutorial.md", "This is a sample *tutorial file*")); html.Should().Contain("tutorial file"); } @@ -136,7 +135,7 @@ public async Task Should_parse_markdown_file_and_set_package_with_fully_resolved ```") }; - var project = new MarkdownProject(dirAccessor, PackageRegistry.CreateForHostedMode()); + var project = new MarkdownProject(dirAccessor, Default.PackageFinder); project.TryGetMarkdownFile(new RelativeFilePath("docs/Readme.md"), out var markdownFile).Should().BeTrue(); var htmlDocument = new HtmlDocument(); htmlDocument.LoadHtml((await markdownFile.ToHtmlContentAsync()).ToString()); @@ -425,8 +424,8 @@ public async Task Non_editable_code_inserts_code_present_in_markdown() [Fact] public async Task Package_option_defaults_to_startup_options() { - var expectedPackage = "console"; - var expectedPackageVersion = "1.2.3"; + const string expectedPackage = "console"; + const string expectedPackageVersion = "1.2.3"; var defaultCodeBlockAnnotations = new StartupOptions( package: expectedPackage, @@ -440,7 +439,7 @@ public async Task Package_option_defaults_to_startup_options() "), ("Program.cs", "") }, - new PackageRegistry(), + Default.PackageFinder, defaultCodeBlockAnnotations ); @@ -464,7 +463,7 @@ protected async Task RenderHtml(params (string, string)[] project) var markdownProject = new MarkdownProject( directoryAccessor, - new PackageRegistry()); + Default.PackageFinder); var markdownFile = markdownProject.GetAllMarkdownFiles().Single(); var html = (await markdownFile.ToHtmlContentAsync()).ToString(); diff --git a/MLS.Agent.Tests/Markdown/MarkdownProjectTests.cs b/MLS.Agent.Tests/Markdown/MarkdownProjectTests.cs index 6c0d604a1..c4b856831 100644 --- a/MLS.Agent.Tests/Markdown/MarkdownProjectTests.cs +++ b/MLS.Agent.Tests/Markdown/MarkdownProjectTests.cs @@ -29,7 +29,7 @@ public void Returns_list_of_all_relative_paths_to_all_markdown_files() ("Program.cs", "") }; - var project = new MarkdownProject(dirAccessor, PackageRegistry.CreateForHostedMode()); + var project = new MarkdownProject(dirAccessor, Default.PackageFinder); var files = project.GetAllMarkdownFiles(); @@ -46,7 +46,7 @@ public void Returns_false_for_nonexistent_file() { var workingDir = TestAssets.SampleConsole; var dirAccessor = new InMemoryDirectoryAccessor(workingDir); - var project = new MarkdownProject(dirAccessor, PackageRegistry.CreateForHostedMode()); + var project = new MarkdownProject(dirAccessor, Default.PackageFinder); var path = new RelativeFilePath("DOESNOTEXIST"); project.TryGetMarkdownFile(path, out _).Should().BeFalse(); @@ -70,7 +70,7 @@ public async Task Returns_all_projects_referenced_from_all_markdown_files() ("../Project1/Console1.csproj", @""), ("../Project2/Console2.csproj", @"") }, - PackageRegistry.CreateForHostedMode()); + Default.PackageFinder); var markdownFiles = project.GetAllMarkdownFiles(); diff --git a/MLS.Agent.Tests/WorkspaceDiscoveryTests.cs b/MLS.Agent.Tests/WorkspaceDiscoveryTests.cs index 67f7f203c..5eb9bf049 100644 --- a/MLS.Agent.Tests/WorkspaceDiscoveryTests.cs +++ b/MLS.Agent.Tests/WorkspaceDiscoveryTests.cs @@ -7,6 +7,7 @@ using System.IO; using System.Threading.Tasks; using Clockwise; +using FluentAssertions.Extensions; using Microsoft.DotNet.Try.Protocol; using Microsoft.DotNet.Try.Protocol.Tests; using MLS.Agent.CommandLine; @@ -46,13 +47,16 @@ public async Task Local_tool_workspace_can_be_discovered() [Fact] public async Task Project_file_path_workspace_can_be_discovered_and_run_with_buffer_inlining() { - var workspace = (await Create.ConsoleWorkspaceCopy()).Directory; + var package = Create.EmptyWorkspace("a space"); + var build = await Create.NewPackage(package.Name, package.Directory, Create.ConsoleConfiguration) as IHaveADirectory; + + var workspace = build.Directory; var csproj = workspace.GetFiles("*.csproj")[0]; var programCs = workspace.GetFiles("*.cs")[0]; var output = Guid.NewGuid().ToString(); var ws = new Workspace( - files: new[] { new File(programCs.FullName, SourceCodeProvider.ConsoleProgramSingleRegion) }, + files: new[] { new File(programCs.FullName, SourceCodeProvider.ConsoleProgramSingleRegion) }, buffers: new[] { new Buffer(new BufferId(programCs.FullName, "alpha"), $"Console.WriteLine(\"{output}\");") }, workspaceType: csproj.FullName); @@ -66,24 +70,19 @@ public async Task Project_file_path_workspace_can_be_discovered_and_run_with_buf result.ShouldSucceedWithOutput(output); } - private async Task<(string packageName, DirectoryInfo addSource)> CreateLocalTool(IConsole console) + private async Task<(string packageName, DirectoryInfo packageLocation)> CreateLocalTool(IConsole console) { // Keep project name short to work around max path issues var projectName = Guid.NewGuid().ToString("N").Substring(0, 8); + var build = await Create.NewPackage(projectName, Create.ConsoleConfiguration) as IHaveADirectory; - var copy = Create.EmptyWorkspace( - initializer: new PackageInitializer( - "console", - projectName)); - - await copy.CreateRoslynWorkspaceForRunAsync(new Budget()); - + var ws = await ((ICreateWorkspaceForRun)build).CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); var packageLocation = new DirectoryInfo( - Path.Combine(copy.Directory.FullName, "pack-output")); + Path.Combine(build.Directory.FullName, "pack-output")); var packageName = await PackCommand.Do( new PackOptions( - copy.Directory, + build.Directory, outputDirectory: packageLocation, enableBlazor: false), console); diff --git a/MLS.Agent/MLS.Agent.v3.ncrunchproject b/MLS.Agent/MLS.Agent.v3.ncrunchproject new file mode 100644 index 000000000..65923ee2a --- /dev/null +++ b/MLS.Agent/MLS.Agent.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + + ..\docs\**.* + ..\MLS.Styles\**.* + + + \ No newline at end of file diff --git a/MLS.Agent/Markdown/MarkdownProject.cs b/MLS.Agent/Markdown/MarkdownProject.cs index f033b6329..58a6e33df 100644 --- a/MLS.Agent/Markdown/MarkdownProject.cs +++ b/MLS.Agent/Markdown/MarkdownProject.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; using Markdig; using Microsoft.DotNet.Try.Markdown; using Recipes; @@ -40,6 +42,11 @@ public IEnumerable GetAllFilesRecursively() return Enumerable.Empty(); } + public IEnumerable GetAllFiles() + { + return Enumerable.Empty(); + } + public IEnumerable GetAllDirectoriesRecursively() { return Enumerable.Empty(); @@ -55,6 +62,11 @@ public IDirectoryAccessor GetDirectoryAccessorForRelativePath(RelativeDirectoryP return this; } + public Task TryLockAsync() + { + return Task.FromResult(Disposable.Empty); + } + public void WriteAllText(RelativeFilePath path, string text) { } diff --git a/WorkspaceServer.Tests/AspNetWorkspaceTests.cs b/WorkspaceServer.Tests/AspNetWorkspaceTests.cs index 9148d4daf..5081e6a68 100644 --- a/WorkspaceServer.Tests/AspNetWorkspaceTests.cs +++ b/WorkspaceServer.Tests/AspNetWorkspaceTests.cs @@ -36,9 +36,11 @@ public AspNetWorkspaceTests(ITestOutputHelper output) [Fact] public async Task Run_starts_the_kestrel_server_and_provides_a_WebServer_feature_that_can_receive_requests() { - var (server, package) = await GetRunnerAndWorkspace(); + var registry = Default.PackageFinder; + var server = new RoslynWorkspaceServer(registry); + var package = await registry.Get("aspnet.webapi"); - var workspace = WorkspaceFactory.CreateWorkspaceFromDirectory(package.Directory, package.Name); + var workspace = WorkspaceFactory.CreateWorkspaceFromDirectory(package.Directory, "aspnet.webapi"); using (var runResult = await server.Run(new WorkspaceRequest(workspace, "Program.cs"))) { @@ -52,15 +54,5 @@ public async Task Run_starts_the_kestrel_server_and_provides_a_WebServer_feature result.Should().Equal("value1", "value2"); } } - - protected async Task<(ICodeRunner server, Package workspace)> GetRunnerAndWorkspace( - [CallerMemberName] string testName = null) - { - var package = await Create.WebApiWorkspaceCopy(testName); - - var server = new RoslynWorkspaceServer(new PackageRegistry()); - - return (server, package); - } } } diff --git a/WorkspaceServer.Tests/Create.cs b/WorkspaceServer.Tests/Create.cs index d4db9a38a..674ede4e9 100644 --- a/WorkspaceServer.Tests/Create.cs +++ b/WorkspaceServer.Tests/Create.cs @@ -8,38 +8,80 @@ using System.Reactive.Concurrency; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Clockwise; using Microsoft.DotNet.Try.Protocol; using Microsoft.DotNet.Try.Protocol.Tests; using MLS.Agent.CommandLine; using Recipes; using WorkspaceServer.Packaging; +using WorkspaceServer.Tests.Packaging; using Package = WorkspaceServer.Packaging.Package; namespace WorkspaceServer.Tests { public static class Create { + public static Action ConsoleConfiguration { get; } = packageBuilder => + { + packageBuilder.CreateUsingDotnet("console"); + packageBuilder.TrySetLanguageVersion("8.0"); + packageBuilder.AddPackageReference("Newtonsoft.Json"); + }; + + public static Task NewPackage(string name, Action configure = null, bool createRebuildablePackage = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + var package = EmptyWorkspace(name); + return NewPackage(package.Name, package.Directory, configure, createRebuildablePackage); + } + + public static async Task NewPackage(string name, DirectoryInfo directory, Action configure = null, bool createRebuildablePackage = false) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); + } + if (directory == null) + { + throw new ArgumentNullException(nameof(directory)); + } + + var packageBuilder = new PackageBuilder(name) + { + Directory = directory, + CreateRebuildablePackage = createRebuildablePackage + }; + + + configure?.Invoke(packageBuilder); + var package = packageBuilder.GetPackage(); + + await package.EnsureReady(new Budget()); + + return package; + } + public static async Task ConsoleWorkspaceCopy([CallerMemberName] string testName = null, bool isRebuildable = false, IScheduler buildThrottleScheduler = null) => - await Package.Copy( + await PackageUtilities.Copy( await Default.ConsoleWorkspace(), testName, isRebuildable, buildThrottleScheduler); public static async Task WebApiWorkspaceCopy([CallerMemberName] string testName = null) => - await Package.Copy( + await PackageUtilities.Copy( await Default.WebApiWorkspace(), testName); public static async Task XunitWorkspaceCopy([CallerMemberName] string testName = null) => - await Package.Copy( + await PackageUtilities.Copy( await Default.XunitWorkspace(), testName); public static async Task NetstandardWorkspaceCopy( [CallerMemberName] string testName = null, DirectoryInfo parentDirectory = null) => - await Package.Copy( + await PackageUtilities.Copy( await Default.NetstandardWorkspace(), testName, parentDirectory: parentDirectory); @@ -48,10 +90,10 @@ public static Package EmptyWorkspace([CallerMemberName] string testName = null, { if (!isRebuildablePackage) { - return new NonrebuildablePackage(directory: Package.CreateDirectory(testName), initializer: initializer); + return new NonrebuildablePackage(directory: PackageUtilities.CreateDirectory(testName), initializer: initializer); } - return new RebuildablePackage(directory: Package.CreateDirectory(testName), initializer: initializer); + return new RebuildablePackage(directory: PackageUtilities.CreateDirectory(testName), initializer: initializer); } public static async Task<(string packageName, DirectoryInfo addSource)> NupkgWithBlazorEnabled([CallerMemberName] string testName = null) diff --git a/WorkspaceServer.Tests/Default.cs b/WorkspaceServer.Tests/Default.cs index fd5e9fc8e..418331a0d 100644 --- a/WorkspaceServer.Tests/Default.cs +++ b/WorkspaceServer.Tests/Default.cs @@ -1,6 +1,8 @@ // 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 WorkspaceServer.Packaging; @@ -8,14 +10,14 @@ namespace WorkspaceServer.Tests { public static class Default { - private static readonly PackageRegistry DefaultPackages = PackageRegistry.CreateForHostedMode(); + public static PackageRegistry PackageFinder { get; } = PackageRegistry.CreateForHostedMode(); - public static async Task ConsoleWorkspace() => await DefaultPackages.Get("console"); + public static async Task ConsoleWorkspace() => await PackageFinder.Get("console"); - public static async Task WebApiWorkspace() => await DefaultPackages.Get("aspnet.webapi"); + public static async Task WebApiWorkspace() => await PackageFinder.Get("aspnet.webapi"); - public static async Task XunitWorkspace() => await DefaultPackages.Get("xunit"); + public static async Task XunitWorkspace() => await PackageFinder.Get("xunit"); - public static async Task NetstandardWorkspace() => await DefaultPackages.Get("blazor-console"); + public static async Task NetstandardWorkspace() => await PackageFinder.Get("blazor-console"); } } \ No newline at end of file diff --git a/WorkspaceServer.Tests/InMemoryDirectoryAccessor.cs b/WorkspaceServer.Tests/InMemoryDirectoryAccessor.cs index e02fc874b..50391552d 100644 --- a/WorkspaceServer.Tests/InMemoryDirectoryAccessor.cs +++ b/WorkspaceServer.Tests/InMemoryDirectoryAccessor.cs @@ -6,12 +6,15 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.DotNet.Try.Markdown; +using Recipes; namespace WorkspaceServer.Tests { public class InMemoryDirectoryAccessor : IDirectoryAccessor, IEnumerable { + private readonly AsyncLock _lock = new AsyncLock(); private readonly DirectoryInfo _rootDirToAddFiles; private Dictionary _files = new Dictionary( @@ -144,11 +147,17 @@ public IDirectoryAccessor GetDirectoryAccessorForRelativePath(RelativeDirectoryP { var newPath = WorkingDirectory.Combine(relativePath); return new InMemoryDirectoryAccessor(newPath) - { - _files = _files - }; + { + _files = _files + }; + } + + public Task TryLockAsync() + { + return Task.FromResult(_lock.LockAsync()); } + public IEnumerable GetAllDirectoriesRecursively() { return _files.Keys @@ -157,6 +166,15 @@ public IEnumerable GetAllDirectoriesRecursively() Path.GetRelativePath(WorkingDirectory.FullName, key.FullName))); } + public IEnumerable GetAllFiles() + { + return _files.Keys + .OfType() + .Where(key => key.Directory.FullName == WorkingDirectory.FullName) + .Select(key => new RelativeFilePath( + Path.GetRelativePath(WorkingDirectory.FullName, key.FullName))); + } + public IEnumerable GetAllFilesRecursively() { return _files.Keys diff --git a/WorkspaceServer.Tests/NetstandardWorkspaceTests.cs b/WorkspaceServer.Tests/NetstandardWorkspaceTests.cs index 694dfde9f..aca877d3b 100644 --- a/WorkspaceServer.Tests/NetstandardWorkspaceTests.cs +++ b/WorkspaceServer.Tests/NetstandardWorkspaceTests.cs @@ -4,22 +4,16 @@ using System; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; -using System.Text; using System.Threading.Tasks; using Clockwise; using FluentAssertions; using Microsoft.DotNet.Try.Protocol; using Pocket; -using WorkspaceServer.Models.Execution; using WorkspaceServer.Servers.Roslyn; using WorkspaceServer.Tests.CodeSamples; -using WorkspaceServer.Packaging; using Xunit; using Xunit.Abstractions; -using static Pocket.Logger; using Buffer = Microsoft.DotNet.Try.Protocol.Buffer; -using Package = WorkspaceServer.Packaging.Package; namespace WorkspaceServer.Tests { @@ -38,10 +32,10 @@ public NetstandardWorkspaceTests(ITestOutputHelper output) [Fact] public async Task When_run_fails_to_compile_then_diagnostics_are_aligned_with_buffer_span() { - var (server, build) = await GetRunnerAndWorkspace(); + var server = GetCodeCompiler(); var workspace = new Workspace( - workspaceType: build.Name, + workspaceType: "blazor-console", files: new[] { new File("Program.cs", SourceCodeProvider.ConsoleProgramSingleRegion) }, buffers: new[] { new Buffer("Program.cs@alpha", @"Console.WriteLine(banana);", 0) }); @@ -60,10 +54,10 @@ public async Task When_run_fails_to_compile_then_diagnostics_are_aligned_with_bu [Fact] public async Task Compile_with_active_buffer_id_includes_diagnostics_on_edge_of_region() { - var (server, build) = await GetRunnerAndWorkspace(); + var server = GetCodeCompiler(); var workspace = new Workspace( - workspaceType: build.Name, + workspaceType: "blazor-console", files: new[] { new File("Program.cs", "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nnamespace MyCodeSample\r\n{\r\npublic class Program\r\n {\r\n public static void Main()\r\n {\r\n #region code\r\n #endregion\r\n }\r\n }\r\n}") }, buffers: new[] { new Buffer("Program.cs@code", @"var x = 3", 0) }); @@ -81,10 +75,10 @@ public async Task Compile_with_active_buffer_id_includes_diagnostics_on_edge_of_ [Fact] public async Task Compile_can_succeed_and_run() { - var (server, build) = await GetRunnerAndWorkspace(); + var server = GetCodeCompiler(); var workspace = new Workspace( - workspaceType: build.Name, + workspaceType: "blazor-console", files: new[] { new File("Program.cs", SourceCodeProvider.ConsoleProgramSingleRegion) }, buffers: new[] { new Buffer("Program.cs@alpha", @"Console.WriteLine(2);", 0) }); @@ -93,7 +87,7 @@ public async Task Compile_can_succeed_and_run() result.Succeeded.Should().BeTrue(); - var bytes = System.Convert.FromBase64String(result.Base64Assembly); + var bytes = Convert.FromBase64String(result.Base64Assembly); var assembly = Assembly.Load(bytes); var main = assembly.GetTypes(). SelectMany(t => t.GetMethods()) @@ -102,14 +96,6 @@ public async Task Compile_can_succeed_and_run() main.Invoke(null, new [] { new string[] { } }); } - protected async Task<(ICodeCompiler server, Package workspace)> GetRunnerAndWorkspace( - [CallerMemberName] string testName = null) - { - var workspace = await Create.NetstandardWorkspaceCopy(testName); - - var server = new RoslynWorkspaceServer(workspace); - - return (server, workspace); - } + protected ICodeCompiler GetCodeCompiler() => new RoslynWorkspaceServer(Default.PackageFinder); } } diff --git a/WorkspaceServer.Tests/PackageRegistryTests.cs b/WorkspaceServer.Tests/PackageRegistryTests.cs index 48d3d0614..49a8f8c42 100644 --- a/WorkspaceServer.Tests/PackageRegistryTests.cs +++ b/WorkspaceServer.Tests/PackageRegistryTests.cs @@ -4,26 +4,27 @@ using System.Threading.Tasks; using FluentAssertions; using WorkspaceServer.Packaging; +using WorkspaceServer.Tests.Packaging; using Xunit; namespace WorkspaceServer.Tests { public class PackageRegistryTests { - private readonly PackageRegistry registry = new PackageRegistry(); + private readonly PackageRegistry _registry = new PackageRegistry(); [Fact(Skip = "Cache is disabled for the moment")] public async Task PackageRegistry_will_return_same_instance_of_a_package() { // FIX: (PackageRegistry_will_return_same_instance_of_a_package) - var packageName = Package.CreateDirectory(nameof(PackageRegistry_will_return_same_instance_of_a_package)).Name; + var packageName = PackageUtilities.CreateDirectory(nameof(PackageRegistry_will_return_same_instance_of_a_package)).Name; - registry.Add(packageName, + _registry.Add(packageName, options => options.CreateUsingDotnet("console")); - var package1 = await registry.Get(packageName); - var package2 = await registry.Get(packageName); + var package1 = await _registry.Get(packageName); + var package2 = await _registry.Get(packageName); package1.Should().BeSameAs(package2); } diff --git a/WorkspaceServer.Tests/PackageTests.cs b/WorkspaceServer.Tests/PackageTests.cs index f099bd52a..79fe3afe7 100644 --- a/WorkspaceServer.Tests/PackageTests.cs +++ b/WorkspaceServer.Tests/PackageTests.cs @@ -13,20 +13,21 @@ using Xunit.Abstractions; using WorkspaceServer.Packaging; using System.Threading; +using WorkspaceServer.Tests.Packaging; namespace WorkspaceServer.Tests { public partial class PackageTests : IDisposable { - private readonly CompositeDisposable disposables = new CompositeDisposable(); + private readonly CompositeDisposable _disposables = new CompositeDisposable(); public PackageTests(ITestOutputHelper output) { - disposables.Add(output.SubscribeToPocketLogger()); - disposables.Add(VirtualClock.Start()); + _disposables.Add(output.SubscribeToPocketLogger()); + _disposables.Add(VirtualClock.Start()); } - public void Dispose() => disposables.Dispose(); + public void Dispose() => _disposables.Dispose(); [Fact] public async Task A_package_is_not_initialized_more_than_once() @@ -76,7 +77,7 @@ public async Task A_package_copy_is_not_reinitialized_if_the_source_was_already_ await original.CreateRoslynWorkspaceForLanguageServicesAsync(new TimeBudget(30.Seconds())); - var copy = await Package.Copy(original); + var copy = await PackageUtilities.Copy(original); await copy.CreateRoslynWorkspaceForLanguageServicesAsync(new TimeBudget(30.Seconds())); @@ -176,11 +177,11 @@ await Task.WhenAll( [Theory] [InlineData("console", false)] [InlineData("nodatime.api", false)] - [InlineData("blazor-console", true, Skip = "Requires package design changes")] - [InlineData("blazor-nodatime.api", true, Skip = "Requires package design changes")] + //[InlineData("blazor-console", true, Skip = "Requires package design changes")] + //[InlineData("blazor-nodatime.api", true, Skip = "Requires package design changes")] public async Task CanSupportBlazor_indicates_whether_the_package_supports_Blazor(string packageName, bool expected) { - var registry = PackageRegistry.CreateForHostedMode(); + var registry = Default.PackageFinder; var package = await registry.Get(packageName); package.CanSupportBlazor.Should().Be(expected); } diff --git a/WorkspaceServer.Tests/PackageTests2.cs b/WorkspaceServer.Tests/PackageTests2.cs index 6786886c1..37bf02644 100644 --- a/WorkspaceServer.Tests/PackageTests2.cs +++ b/WorkspaceServer.Tests/PackageTests2.cs @@ -19,8 +19,8 @@ public void It_can_have_assets_added_to_it() var directoryAccessor = new InMemoryDirectoryAccessor(); var package = new Package2("the-package", directoryAccessor); - - var projectAsset = new ProjectAsset(directoryAccessor); + directoryAccessor.Add(("mainProject.csproj","")); + var projectAsset = new ProjectAsset(directoryAccessor, "mainProject.csproj"); package.Add(projectAsset); package.Assets.Should().Contain(a => a == projectAsset); @@ -30,10 +30,10 @@ public void It_can_have_assets_added_to_it() public void An_asset_must_be_in_a_subdirectory_of_the_package() { var directoryAccessor = new InMemoryDirectoryAccessor(); - + directoryAccessor.Add(("./2/mainProject.csproj", "")); var package = new Package2("1", directoryAccessor.GetDirectoryAccessorForRelativePath("1")); - var projectAsset = new ProjectAsset(directoryAccessor.GetDirectoryAccessorForRelativePath("2")); + var projectAsset = new ProjectAsset(directoryAccessor.GetDirectoryAccessorForRelativePath("2"), "mainProject.csproj"); package.Invoking(p => p.Add(projectAsset)).Should() .Throw() diff --git a/WorkspaceServer.Tests/Packaging/PackageUtilities.cs b/WorkspaceServer.Tests/Packaging/PackageUtilities.cs new file mode 100644 index 000000000..7b992a2a1 --- /dev/null +++ b/WorkspaceServer.Tests/Packaging/PackageUtilities.cs @@ -0,0 +1,89 @@ +// 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.Reactive.Concurrency; +using System.Threading.Tasks; +using Clockwise; +using MLS.Agent.Tools; +using WorkspaceServer.Packaging; + +namespace WorkspaceServer.Tests.Packaging +{ + public static class PackageUtilities + { + private static readonly object CreateDirectoryLock = new object(); + + public static async Task Copy( + Package fromPackage, + string folderNameStartsWith = null, + bool isRebuildable = false, + IScheduler buildThrottleScheduler = null, + DirectoryInfo parentDirectory = null) + { + if (fromPackage == null) + { + throw new ArgumentNullException(nameof(fromPackage)); + } + + await fromPackage.EnsureReady(new Budget()); + + folderNameStartsWith = folderNameStartsWith ?? fromPackage.Name; + parentDirectory = parentDirectory ?? fromPackage.Directory.Parent; + + var destination = + CreateDirectory(folderNameStartsWith, + parentDirectory); + + fromPackage.Directory.CopyTo(destination); + + var binLogs = destination.GetFiles("*.binlog"); + + foreach (var fileInfo in binLogs) + { + fileInfo.Delete(); + } + + Package copy; + if (isRebuildable) + { + copy = new RebuildablePackage(directory: destination, name: destination.Name, buildThrottleScheduler: buildThrottleScheduler); + } + else + { + copy = new NonrebuildablePackage(directory: destination, name: destination.Name, buildThrottleScheduler: buildThrottleScheduler); + } + + return copy; + } + + public static DirectoryInfo CreateDirectory( + string folderNameStartsWith, + DirectoryInfo parentDirectory = null) + { + if (string.IsNullOrWhiteSpace(folderNameStartsWith)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(folderNameStartsWith)); + } + + parentDirectory = parentDirectory ?? Package.DefaultPackagesDirectory; + + DirectoryInfo created; + + lock (CreateDirectoryLock) + { + if (!parentDirectory.Exists) + { + parentDirectory.Create(); + } + + var existingFolders = parentDirectory.GetDirectories($"{folderNameStartsWith}.*"); + + created = parentDirectory.CreateSubdirectory($"{folderNameStartsWith}.{existingFolders.Length + 1}"); + } + + return created; + } + } +} diff --git a/WorkspaceServer.Tests/PipelineStepTests.cs b/WorkspaceServer.Tests/PipelineStepTests.cs new file mode 100644 index 000000000..6fd839a43 --- /dev/null +++ b/WorkspaceServer.Tests/PipelineStepTests.cs @@ -0,0 +1,191 @@ +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Extensions; +using WorkspaceServer.Packaging; +using Xunit; + +namespace WorkspaceServer.Tests +{ + public class PipelineStepTests + { + [Fact] + public async Task It_produces_a_new_value_when_there_is_none() + { + var producer = new PipelineStep(() => Task.FromResult("something")); + var value = await producer.GetLatestAsync(); + value.Should().Be("something"); + } + + [Fact] + public async Task It_produces_a_new_value_when_invalidated() + { + var seed = 0; + var producer = new PipelineStep(() => Task.FromResult(Interlocked.Increment(ref seed))); + var value = await producer.GetLatestAsync(); + value.Should().Be(1); + + producer.Invalidate(); + var newValue = await producer.GetLatestAsync(); + newValue.Should().Be(2); + } + + [Fact] + public async Task It_propagates_exception() + { + var producer = new PipelineStep( () => throw new InvalidOperationException()); + producer.Awaiting(p => p.GetLatestAsync()) + .Should() + .Throw(); + + } + + [Fact] + public async Task It_remains_invalid_if_exceptions_are_thrown() + { + var seed = 0; + var producer = new PipelineStep(() => + { + var next = Interlocked.Increment(ref seed); + if (next == 1) + { + throw new InvalidOperationException(); + } + return Task.FromResult(next); + }); + + producer.Awaiting(p => p.GetLatestAsync()) + .Should() + .Throw(); + + var value = await producer.GetLatestAsync(); + value.Should().Be(2); + + } + + [Fact] + public async Task It_does_not_produces_a_new_value_when_invalidated_until_asked_for_latest_value() + { + var seed = 0; + var producer = new PipelineStep(() => Task.FromResult(Interlocked.Increment(ref seed))); + var value = await producer.GetLatestAsync(); + value.Should().Be(1); + producer.Invalidate(); + seed.Should().Be(1); + } + + [Fact] + public async Task It_retains_the_latest_value() + { + var seed = 0; + var producer = new PipelineStep(() => Task.FromResult(Interlocked.Increment(ref seed))); + var value1 = await producer.GetLatestAsync(); + var value2 = await producer.GetLatestAsync(); + var value3 = await producer.GetLatestAsync(); + value1.Should().Be(1); + value2.Should().Be(1); + value3.Should().Be(1); + } + + [Fact] + public async Task It_returns_same_value_to_concurrent_requests() + { + var seed = 0; + var barrier = new Barrier(3); + var producer = new PipelineStep(() => + { + barrier.SignalAndWait(2.Seconds()); + return Task.FromResult(++seed); + }); + + var values = await Task.WhenAll( Enumerable.Range(0, 3) + .AsParallel() + .Select((_) => producer.GetLatestAsync())); + values.Should().HaveCount(3).And.OnlyContain(i => i == 1); + } + + [Fact] + public async Task When_invalidated_while_producing_a_value_the_consumer_waiting_will_wait_for_latest_production_to_be_finished() + { + var seed = 0; + var consumerBarrier = new Barrier(2); + var producerBarrier = new Barrier(2); + + var producer = new PipelineStep(() => + { + // will require all consumer to reach this point to move on + producerBarrier.SignalAndWait(2.Seconds()); + return Task.FromResult(Interlocked.Increment(ref seed)); + }); + + var firstConsumer = Task.Run(() => + { + var task = producer.GetLatestAsync(); + // block waiting for the other consumer + consumerBarrier.SignalAndWait(2.Seconds()); + return task; + } + ); + + var secondConsumer = Task.Run(() => + { + // now both consumer reached the barrier + consumerBarrier.SignalAndWait(2.Seconds()); + producer.Invalidate(); + // let the firs request fire + producerBarrier.RemoveParticipant(); + // second request after invalidation + var task = producer.GetLatestAsync(); + return task; + } + ); + + var values = await Task.WhenAll(firstConsumer, secondConsumer); + values.Should().HaveCount(2).And.OnlyContain(i => i == 2); + + } + + [Fact]public async Task Sequence_of_steps_produce_a_value() + { + var step1 = new PipelineStep(()=>Task.FromResult(1)); + var step2 = step1.Then( (number) => Task.FromResult($"{number} {number}") ); + var value1 = await step2.GetLatestAsync(); + value1.Should().Be("1 1"); + } + + [Fact] + public async Task Invalidating_a_step_in_a_sequence_causes_only_that_step_to_re_evaluate() + { + var seed1 = 0; + var step1 = new PipelineStep(() => Task.FromResult(Interlocked.Increment(ref seed1))); + var seed2 = 0; + var step2 = step1.Then((number) => Task.FromResult($"{number} {Interlocked.Increment(ref seed2)}")); + await step2.GetLatestAsync(); + + step2.Invalidate(); + var value = await step2.GetLatestAsync(); + value.Should().Be("1 2"); + } + + [Fact] + public async Task Invalidating_a_step_in_a_sequence_causes_all_successor_to_evaluate() + { + var seed1 = 0; + var seed2 = 0; + var seed3 = 0; + var step1 = new PipelineStep(() => Task.FromResult(Interlocked.Increment(ref seed1))); + var step2 = step1.Then((number) => Task.FromResult($"{number} {Interlocked.Increment(ref seed2)}")); + var step3 = step2.Then((text) => Task.FromResult($"{text} {Interlocked.Increment(ref seed3)}")); + await step3.GetLatestAsync(); + + step2.Invalidate(); + var value = await step3.GetLatestAsync(); + value.Should().Be("1 2 2"); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer.Tests/RebuildablePackageTests.cs b/WorkspaceServer.Tests/RebuildablePackageTests.cs index 58f2e3839..bc473ad89 100644 --- a/WorkspaceServer.Tests/RebuildablePackageTests.cs +++ b/WorkspaceServer.Tests/RebuildablePackageTests.cs @@ -35,20 +35,22 @@ public async Task If_a_new_file_is_added_the_workspace_includes_the_file() var ws = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); var newFile = Path.Combine(package.Directory.FullName, "Sample.cs"); - ws.CurrentSolution.Projects.First().Documents.Should().NotContain(d => d.FilePath == newFile); + ws.CurrentSolution.Projects.First().Documents.Select(d => d.FilePath).Should().NotContain(filePath => filePath == newFile); File.WriteAllText(newFile, "//this is a new file"); ws = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); - ws.CurrentSolution.Projects.First().Documents.Should().Contain(d => d.FilePath == newFile); + ws.CurrentSolution.Projects.First().Documents.Select(d => d.FilePath).Should().Contain(filePath => filePath == newFile); } [Fact] public async Task If_the_project_file_is_changed_then_the_workspace_reflects_the_changes() { - var package = (RebuildablePackage)await Create.ConsoleWorkspaceCopy(isRebuildable: true); - var ws = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); + var package = Create.EmptyWorkspace(); + var build = await Create.NewPackage(package.Name, package.Directory, Create.ConsoleConfiguration) as ICreateWorkspaceForRun; + + var ws = await build.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); var references = ws.CurrentSolution.Projects.First().MetadataReferences; references.Should().NotContain(reference => @@ -67,17 +69,19 @@ public async Task If_the_project_file_is_changed_then_the_workspace_reflects_the [Fact] public async Task If_an_existing_file_is_deleted_then_the_workspace_does_not_include_the_file() { - var package = (RebuildablePackage)await Create.ConsoleWorkspaceCopy(isRebuildable: true); - var ws = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); + var package = Create.EmptyWorkspace(); + var build = await Create.NewPackage(package.Name, package.Directory, Create.ConsoleConfiguration, true) as ICreateWorkspaceForRun; + + var ws = await build.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); var existingFile = Path.Combine(package.Directory.FullName, "Program.cs"); - ws.CurrentSolution.Projects.First().Documents.Should().Contain(d => d.FilePath == existingFile); + ws.CurrentSolution.Projects.First().Documents.Select(d => d.FilePath).Should().Contain(filePath => filePath == existingFile); File.Delete(existingFile); await Task.Delay(1000); - ws = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); - ws.CurrentSolution.Projects.First().Documents.Should().NotContain(d => d.FilePath == existingFile); + ws = await build.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); + ws.CurrentSolution.Projects.First().Documents.Select(d => d.FilePath).Should().NotContain(filePath => filePath == existingFile); } [Fact] @@ -113,8 +117,8 @@ public async Task If_a_build_is_in_progress_and_another_request_comes_in_both_ar var workspaces = await Task.WhenAll(workspace1, workspace2); - workspaces[0].CurrentSolution.Projects.First().Documents.Should().Contain(p => p.FilePath.EndsWith("Sample.cs")); - workspaces[1].CurrentSolution.Projects.First().Documents.Should().Contain(p => p.FilePath.EndsWith("Sample.cs")); + workspaces[0].CurrentSolution.Projects.First().Documents.Select(d => d.FilePath).Should().Contain(filePath => filePath.EndsWith("Sample.cs")); + workspaces[1].CurrentSolution.Projects.First().Documents.Select(d => d.FilePath).Should().Contain(filePath => filePath.EndsWith("Sample.cs")); } } } diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectTests.cs index da321d7af..f8dc8d2ff 100644 Binary files a/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectTests.cs and b/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectTests.cs differ diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs index 4fbbc98ae..250cdb91a 100644 --- a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs +++ b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptDiagnosticsTests.cs @@ -1,8 +1,6 @@ // 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.Runtime.CompilerServices; using System.Threading.Tasks; using FluentAssertions; using Microsoft.DotNet.Try.Protocol; @@ -22,11 +20,6 @@ public RoslynWorkspaceServerScriptDiagnosticsTests(ITestOutputHelper output) : b { } - protected override Task<(ICodeRunner runner, Package workspace)> GetRunnerAndWorkspaceBuild(string testName = null) - { - return Task.FromResult(((ICodeRunner)new ScriptingWorkspaceServer(),(Package) new NonrebuildablePackage("script"))); - } - [Fact] public async Task Get_diagnostics() { @@ -41,8 +34,10 @@ public async Task Get_diagnostics() result.Diagnostics.Should().Contain(diagnostics => diagnostics.Message == "(1,1): error CS0103: The name \'addd\' does not exist in the current context"); } - protected override ILanguageService GetLanguageService( - [CallerMemberName] string testName = null) => new RoslynWorkspaceServer( - PackageRegistry.CreateForHostedMode()); + protected override ILanguageService GetLanguageService() => new RoslynWorkspaceServer(Default.PackageFinder); + + protected override ICodeCompiler GetCodeCompiler() => new RoslynWorkspaceServer(Default.PackageFinder); + + protected override ICodeRunner GetCodeRunner() => new RoslynWorkspaceServer(Default.PackageFinder); } } \ No newline at end of file diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs index 5c79645cc..c928f7fbf 100644 --- a/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs +++ b/WorkspaceServer.Tests/RoslynWorkspaceServerScriptIntellisenseTests.cs @@ -1,37 +1,23 @@ // 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.Linq; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using FluentAssertions; using Microsoft.DotNet.Try.Protocol; using Microsoft.DotNet.Try.Protocol.Tests; -using WorkspaceServer.Servers.Roslyn; -using WorkspaceServer.Servers.Scripting; -using WorkspaceServer.Packaging; using Xunit; using Xunit.Abstractions; using Buffer = Microsoft.DotNet.Try.Protocol.Buffer; -using Package = WorkspaceServer.Packaging.Package; namespace WorkspaceServer.Tests { - public class RoslynWorkspaceServerScriptIntellisenseTests : WorkspaceServerTestsCore + public class RoslynWorkspaceServerScriptIntellisenseTests : RoslynWorkspaceServerTestsCore { public RoslynWorkspaceServerScriptIntellisenseTests(ITestOutputHelper output) : base(output) { } - protected override Task<(ICodeRunner runner, Package workspace)> GetRunnerAndWorkspaceBuild(string testName = null) - { - return Task.FromResult(((ICodeRunner)new ScriptingWorkspaceServer(),(Package) new NonrebuildablePackage("script"))); - } - protected override ILanguageService GetLanguageService( - [CallerMemberName] string testName = null) => new RoslynWorkspaceServer( - PackageRegistry.CreateForHostedMode()); - [Fact] public async Task Get_signature_help_for_invalid_location_return_empty() { diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectDiagnosticsTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerTestsConsoleProjectDiagnosticsTests.cs similarity index 85% rename from WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectDiagnosticsTests.cs rename to WorkspaceServer.Tests/RoslynWorkspaceServerTestsConsoleProjectDiagnosticsTests.cs index ff1cae474..1f4cba49a 100644 --- a/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectDiagnosticsTests.cs +++ b/WorkspaceServer.Tests/RoslynWorkspaceServerTestsConsoleProjectDiagnosticsTests.cs @@ -1,23 +1,21 @@ // 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.Runtime.CompilerServices; using System.Threading.Tasks; using FluentAssertions; using Microsoft.DotNet.Try.Protocol; using Microsoft.DotNet.Try.Protocol.Tests; -using WorkspaceServer.Servers.Roslyn; using Xunit; using Xunit.Abstractions; using Buffer = Microsoft.DotNet.Try.Protocol.Buffer; -using Package = WorkspaceServer.Packaging.Package; namespace WorkspaceServer.Tests { - public class RoslynWorkspaceServerConsoleProjectDiagnosticsTests : WorkspaceServerTestsCore + public class RoslynWorkspaceServerTestsConsoleProjectDiagnosticsTests : RoslynWorkspaceServerTestsCore { - public RoslynWorkspaceServerConsoleProjectDiagnosticsTests(ITestOutputHelper output) : base(output) + + + public RoslynWorkspaceServerTestsConsoleProjectDiagnosticsTests(ITestOutputHelper output) : base(output) { } @@ -160,15 +158,6 @@ public static IEnumerable Fibonacci() result.Diagnostics.Should().Contain(diagnostics => diagnostics.Message == "generators/FibonacciGenerator.cs(14,17): error CS0246: The type or namespace name \'adddd\' could not be found (are you missing a using directive or an assembly reference?)"); } - protected override Task<(ICodeRunner runner, Package workspace)> GetRunnerAndWorkspaceBuild( - [CallerMemberName] string testName = null) - { - throw new NotImplementedException(); - } - - protected override ILanguageService GetLanguageService([CallerMemberName] string testName = null) - { - return new RoslynWorkspaceServer(PackageRegistry.CreateForHostedMode()); - } + } } \ No newline at end of file diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectIntellisenseTests.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerTestsConsoleProjectIntellisenseTests.cs similarity index 95% rename from WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectIntellisenseTests.cs rename to WorkspaceServer.Tests/RoslynWorkspaceServerTestsConsoleProjectIntellisenseTests.cs index 756455e17..6047d8f59 100644 --- a/WorkspaceServer.Tests/RoslynWorkspaceServerConsoleProjectIntellisenseTests.cs +++ b/WorkspaceServer.Tests/RoslynWorkspaceServerTestsConsoleProjectIntellisenseTests.cs @@ -1,14 +1,12 @@ // 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.Linq; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using FluentAssertions; using Microsoft.DotNet.Try.Protocol; using Microsoft.DotNet.Try.Protocol.Tests; -using WorkspaceServer.Servers.Roslyn; +using WorkspaceServer.Tests.Packaging; using Xunit; using Xunit.Abstractions; using Buffer = Microsoft.DotNet.Try.Protocol.Buffer; @@ -16,9 +14,9 @@ namespace WorkspaceServer.Tests { - public class RoslynWorkspaceServerConsoleProjectIntellisenseTests : WorkspaceServerTestsCore + public class RoslynWorkspaceServerTestsConsoleProjectIntellisenseTests : RoslynWorkspaceServerTestsCore { - public RoslynWorkspaceServerConsoleProjectIntellisenseTests(ITestOutputHelper output) : base(output) + public RoslynWorkspaceServerTestsConsoleProjectIntellisenseTests(ITestOutputHelper output) : base(output) { } @@ -188,11 +186,10 @@ public static IEnumerable Fibonacci() }".EnforceLF(); #endregion - - var package = await Package.Copy(await Default.ConsoleWorkspace()); + var (processed, position) = CodeManipulation.ProcessMarkup(generator); - var workspace = new Workspace(workspaceType: package.Name, buffers: new[] + var workspace = new Workspace(workspaceType: "console", buffers: new[] { new Buffer("Program.cs", program), new Buffer("generators/FibonacciGenerator.cs", processed, position) @@ -697,7 +694,7 @@ public static IEnumerable Fibonacci() #endregion var (processed, position) = CodeManipulation.ProcessMarkup(generator); - var package = await Package.Copy(await Default.ConsoleWorkspace()); + var package = await PackageUtilities.Copy(await Default.ConsoleWorkspace()); var workspace = new Workspace(workspaceType: package.Name, buffers: new[] { new Buffer("Program.cs", program), @@ -710,7 +707,7 @@ public static IEnumerable Fibonacci() result.Signatures.Should().NotBeNullOrEmpty(); - var sample = result.Signatures.Where(e => e.Label == "void Console.WriteLine(string format, params object[] arg)").First(); + var sample = result.Signatures.First(e => e.Label == "void Console.WriteLine(string format, params object[] arg)"); sample.Documentation.Value.Should().Contain("Writes the text representation of the specified array of objects, followed by the current line terminator, to the standard output stream using the specified format information."); sample.Parameters.Should().HaveCount(2); @@ -722,16 +719,5 @@ public static IEnumerable Fibonacci() sample.Parameters.ElementAt(1).Label.Should().Be("params object[] arg"); sample.Parameters.ElementAt(1).Documentation.Value.Should().Contain("An array of objects to write using format."); } - - protected override Task<(ICodeRunner runner, Package workspace)> GetRunnerAndWorkspaceBuild( - [CallerMemberName] string testName = null) - { - throw new NotImplementedException(); - } - - protected override ILanguageService GetLanguageService([CallerMemberName] string testName = null) - { - return new RoslynWorkspaceServer(PackageRegistry.CreateForHostedMode()); - } } } diff --git a/WorkspaceServer.Tests/RoslynWorkspaceServerTestsCore.cs b/WorkspaceServer.Tests/RoslynWorkspaceServerTestsCore.cs new file mode 100644 index 000000000..2884b21c3 --- /dev/null +++ b/WorkspaceServer.Tests/RoslynWorkspaceServerTestsCore.cs @@ -0,0 +1,22 @@ +// 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 WorkspaceServer.Packaging; +using WorkspaceServer.Servers.Roslyn; +using Xunit.Abstractions; + +namespace WorkspaceServer.Tests +{ + public abstract class RoslynWorkspaceServerTestsCore : WorkspaceServerTestsCore + { + protected RoslynWorkspaceServerTestsCore(ITestOutputHelper output) : base(output) + { + } + + protected override ILanguageService GetLanguageService() => new RoslynWorkspaceServer(Default.PackageFinder); + + protected override ICodeCompiler GetCodeCompiler() => new RoslynWorkspaceServer(Default.PackageFinder); + + protected override ICodeRunner GetCodeRunner() => new RoslynWorkspaceServer(Default.PackageFinder); + } +} \ No newline at end of file diff --git a/WorkspaceServer.Tests/ScriptingWorkspaceServerTests.cs b/WorkspaceServer.Tests/ScriptingWorkspaceServerTests.cs index 5b5c963aa..d6cb34b54 100644 --- a/WorkspaceServer.Tests/ScriptingWorkspaceServerTests.cs +++ b/WorkspaceServer.Tests/ScriptingWorkspaceServerTests.cs @@ -24,16 +24,17 @@ public ScriptingWorkspaceServerTests(ITestOutputHelper output) : base(output) { } - protected override Workspace CreateWorkspaceWithMainContaining(string text, Package package) => + protected override Workspace CreateWorkspaceWithMainContaining(string text) => Workspace.FromSource(text, workspaceType: "script"); + - protected override Task<(ICodeRunner runner, Package workspace)> GetRunnerAndWorkspaceBuild( - [CallerMemberName] string testName = null) => - Task.FromResult<(ICodeRunner , Package )>((new ScriptingWorkspaceServer(), new NonrebuildablePackage("script"))); - - protected override ILanguageService GetLanguageService([CallerMemberName] string testName = null) => + protected override ILanguageService GetLanguageService() => throw new NotImplementedException(); + protected override ICodeCompiler GetCodeCompiler() => throw new NotImplementedException(); + + protected override ICodeRunner GetCodeRunner() => new ScriptingWorkspaceServer(); + [Fact] public async Task Response_shows_fragment_return_value() { @@ -42,7 +43,7 @@ public async Task Response_shows_fragment_return_value() var person = new { Name = ""Jeff"", Age = 20 }; $""{person.Name} is {person.Age} year(s) old""", "script"); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -52,7 +53,7 @@ public async Task Response_shows_fragment_return_value() { Succeeded = true, Output = new string[] { }, - Exception = (string) null, + Exception = (string)null, ReturnValue = $"Jeff is 20 year(s) old", }, config => config.ExcludingMissingMembers()); } @@ -62,7 +63,7 @@ public async Task Response_indicates_when_compile_is_unsuccessful() { var workspace = Workspace.FromSource(@" Console.WriteLine(banana);", "script"); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -92,7 +93,7 @@ public static void Main() workspaceType: "script", usings: new[] { "System.Threading" }); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -100,7 +101,7 @@ public static void Main() { Succeeded = true, Output = new[] { "Hello there!", "" }, - Exception = (string) null, + Exception = (string)null, }, config => config.ExcludingMissingMembers()); } @@ -117,7 +118,7 @@ public static void Main(params int[] args) Console.WriteLine(""Hello there!""); } }", workspaceType: "script"); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -137,7 +138,7 @@ public static void Main() Console.WriteLine(""Hello there!""); } }", workspaceType: "script"); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -164,7 +165,7 @@ static void Main(string[] args) workspaceType: "script", files: new[] { new File("Main.cs", fileCode) }, buffers: new[] { new Buffer(@"Main.cs@toReplace", @"Console.WriteLine(""Hello there!"");", 0) }); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -190,7 +191,7 @@ static void Main(string[] args) files: new[] { new File("Main.cs", fileCode) }, buffers: new[] { new Buffer(@"Main.cs@toReplace", @"Console.WriteLine(banana);", 0) }); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run(new WorkspaceRequest(workspace)); result.Should().BeEquivalentTo(new diff --git a/WorkspaceServer.Tests/WebServerTests.cs b/WorkspaceServer.Tests/WebServerTests.cs index 40e5bcc44..5c10dd74d 100644 --- a/WorkspaceServer.Tests/WebServerTests.cs +++ b/WorkspaceServer.Tests/WebServerTests.cs @@ -13,6 +13,7 @@ using Recipes; using WorkspaceServer.Features; using WorkspaceServer.Packaging; +using WorkspaceServer.Tests.Packaging; using Xunit; using Xunit.Abstractions; @@ -32,7 +33,7 @@ public WebServerTests(ITestOutputHelper output) [Fact] public async Task Multiple_WebServer_instances_can_be_run_concurrently_in_the_same_folder() { - var workspace = await Package.Copy(await Default.WebApiWorkspace()); + var workspace = await PackageUtilities.Copy(await Default.WebApiWorkspace()); using (var webServer1 = new WebServer(workspace)) using (var webServer2 = new WebServer(workspace)) { @@ -47,7 +48,7 @@ public async Task Multiple_WebServer_instances_can_be_run_concurrently_in_the_sa [Fact] public async Task EnsureStarted_returns_the_web_server_base_uri() { - var workspace = await Package.Copy(await Default.WebApiWorkspace()); + var workspace = await PackageUtilities.Copy(await Default.WebApiWorkspace()); using (var webServer = new WebServer(workspace)) { @@ -62,7 +63,7 @@ public async Task EnsureStarted_returns_the_web_server_base_uri() [Fact] public async Task WebServer_lifecycle_events_can_be_viewed_via_StandardOutput() { - var workspace = await Package.Copy(await Default.WebApiWorkspace()); + var workspace = await PackageUtilities.Copy(await Default.WebApiWorkspace()); var log = new StringBuilder(); using (var webServer = new WebServer(workspace)) diff --git a/WorkspaceServer.Tests/WorkspaceServer.Tests.csproj b/WorkspaceServer.Tests/WorkspaceServer.Tests.csproj index 141b0a7ce..2a68da2c0 100644 --- a/WorkspaceServer.Tests/WorkspaceServer.Tests.csproj +++ b/WorkspaceServer.Tests/WorkspaceServer.Tests.csproj @@ -21,6 +21,10 @@ + + all + runtime; build; native; contentfiles; analyzers + diff --git a/WorkspaceServer.Tests/WorkspaceServerRegistryTests.cs b/WorkspaceServer.Tests/WorkspaceServerRegistryTests.cs index 58f2b974e..3d29aa613 100644 --- a/WorkspaceServer.Tests/WorkspaceServerRegistryTests.cs +++ b/WorkspaceServer.Tests/WorkspaceServerRegistryTests.cs @@ -12,34 +12,33 @@ using Xunit; using Xunit.Abstractions; using FluentAssertions.Extensions; -using Microsoft.DotNet.Try.Protocol; using WorkspaceServer.Packaging; -using Package = WorkspaceServer.Packaging.Package; +using WorkspaceServer.Tests.Packaging; namespace WorkspaceServer.Tests { public class WorkspaceServerRegistryTests : IDisposable { - private readonly CompositeDisposable disposables = new CompositeDisposable(); - private readonly PackageRegistry registry = PackageRegistry.CreateForHostedMode(); + private readonly CompositeDisposable _disposables = new CompositeDisposable(); + private readonly PackageRegistry _registry = Default.PackageFinder; public WorkspaceServerRegistryTests(ITestOutputHelper output) { - disposables.Add(output.SubscribeToPocketLogger()); - disposables.Add(VirtualClock.Start()); + _disposables.Add(output.SubscribeToPocketLogger()); + _disposables.Add(VirtualClock.Start()); } - public void Dispose() => disposables.Dispose(); + public void Dispose() => _disposables.Dispose(); - [Fact] + [Fact(Skip = "this api is deprecated, to be replaced")] public async Task Workspaces_can_be_registered_to_be_created_using_dotnet_new() { - var packageName = Package.CreateDirectory(nameof(Workspaces_can_be_registered_to_be_created_using_dotnet_new)).Name; + var packageName = PackageUtilities.CreateDirectory(nameof(Workspaces_can_be_registered_to_be_created_using_dotnet_new)).Name; - registry.Add(packageName, + _registry.Add(packageName, options => options.CreateUsingDotnet("console")); - var package = await registry.Get(packageName); + var package = await _registry.Get(packageName); var workspace = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); @@ -47,51 +46,12 @@ public async Task Workspaces_can_be_registered_to_be_created_using_dotnet_new() project.MetadataReferences.Count.Should().BeGreaterThan(0); } - [Fact] - public async Task NuGet_packages_can_be_added_during_initialization() - { - var workspaceId = Package.CreateDirectory(nameof(NuGet_packages_can_be_added_during_initialization)).Name; - - registry.Add(workspaceId, - options => - { - options.CreateUsingDotnet("console"); - options.AddPackageReference("Twilio", "5.9.2"); - }); - - var workspaceServer = new RoslynWorkspaceServer(registry); - - var workspace = Workspace.FromSource( - @" -using System; -using Twilio.Clients; -using Twilio.Rest.Api.V2010.Account; -using Twilio.Types; - -namespace Twilio_try.dot.net_sample -{ - class Program - { - static void Main() - { - var sendFromPhoneNumber = new PhoneNumber(""TWILIO_PHONE_NUMBER""); - var sendToPhoneNumber = new PhoneNumber(""RECIPIENT_PHONE_NUMBER""); - } - } -}", - workspaceType: workspaceId); - - var result = await workspaceServer.Run(new WorkspaceRequest(workspace)); - - result.Succeeded.Should().BeTrue(because: "compilation can't succeed unless the NuGet package has been restored."); - } - [Fact] public async Task GetWorkspace_will_check_workspaces_directory_if_requested_workspace_was_not_registered() { var unregisteredWorkspace = await Default.ConsoleWorkspace(); - var package = await registry.Get(unregisteredWorkspace.Name); + var package = await _registry.Get(unregisteredWorkspace.Name); var workspace = await package.CreateRoslynWorkspaceForRunAsync(new TimeBudget(30.Seconds())); @@ -102,7 +62,7 @@ public async Task GetWorkspace_will_check_workspaces_directory_if_requested_work public async Task When_workspace_was_not_registered_then_GetWorkspaceServer_will_return_a_working_server() { var unregisteredWorkspace = await Default.ConsoleWorkspace(); - var server = new RoslynWorkspaceServer(registry); + var server = new RoslynWorkspaceServer(_registry); var workspaceRequest = WorkspaceRequestFactory.CreateRequestFromDirectory(unregisteredWorkspace.Directory, unregisteredWorkspace.Name); @@ -114,20 +74,20 @@ public async Task When_workspace_was_not_registered_then_GetWorkspaceServer_will [Fact] public async Task Workspace_can_be_registered_in_directory_other_than_the_default() { - var parentDirectory = Package.CreateDirectory(nameof(Workspace_can_be_registered_in_directory_other_than_the_default)); + var parentDirectory = PackageUtilities.CreateDirectory(nameof(Workspace_can_be_registered_in_directory_other_than_the_default)); var workspaceName = "a"; var childDirectory = parentDirectory.CreateSubdirectory(workspaceName); - registry.Add( + _registry.Add( workspaceName, builder => { builder.Directory = childDirectory; }); - var workspace = await registry.Get(workspaceName); + var workspace = await _registry.Get(workspaceName); workspace.Directory.Should().Be(childDirectory); } diff --git a/WorkspaceServer.Tests/WorkspaceServerTests.cs b/WorkspaceServer.Tests/WorkspaceServerTests.cs index 90964a75b..4906b6e1f 100644 --- a/WorkspaceServer.Tests/WorkspaceServerTests.cs +++ b/WorkspaceServer.Tests/WorkspaceServerTests.cs @@ -12,7 +12,6 @@ using Xunit.Abstractions; using static Pocket.Logger; using DiagnosticSeverity = Microsoft.DotNet.Try.Protocol.DiagnosticSeverity; -using Package = WorkspaceServer.Packaging.Package; using Workspace = Microsoft.DotNet.Try.Protocol.Workspace; namespace WorkspaceServer.Tests @@ -20,21 +19,19 @@ namespace WorkspaceServer.Tests public abstract class WorkspaceServerTests : WorkspaceServerTestsCore { protected abstract Workspace CreateWorkspaceWithMainContaining( - string text, - Package package); + string text); [Fact] public async Task Diagnostic_logs_do_not_show_up_in_captured_console_output() { using (LogEvents.Subscribe(e => Console.WriteLine(e.ToLogString()))) { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var result = await server.Run( new WorkspaceRequest( CreateWorkspaceWithMainContaining( - "Console.WriteLine(\"hi!\");", - build)) + "Console.WriteLine(\"hi!\");")) ); result.Output @@ -52,7 +49,7 @@ protected WorkspaceServerTests(ITestOutputHelper output) : base(output) [Fact] public async Task Response_indicates_when_compile_is_successful_and_signature_is_like_a_console_app() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var workspace = Workspace.FromSource(@" using System; @@ -63,7 +60,7 @@ public static void Main() { } } -", workspaceType: build.Name); +", workspaceType: "console"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -76,8 +73,8 @@ public static void Main() public async Task Response_shows_program_output_when_compile_is_successful_and_signature_is_like_a_console_app() { var output = nameof(Response_shows_program_output_when_compile_is_successful_and_signature_is_like_a_console_app); - - var (server, build) = await GetRunnerAndWorkspaceBuild(); + + var server = GetCodeRunner(); var workspace = Workspace.FromSource($@" using System; @@ -88,7 +85,7 @@ public static void Main() {{ Console.WriteLine(""{output}""); }} -}}", workspaceType: build.Name); +}}", workspaceType: "console"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -99,12 +96,12 @@ public static void Main() [Fact] public async Task Response_shows_program_output_when_compile_is_successful_and_signature_is_a_fragment_containing_console_output() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" var person = new { Name = ""Jeff"", Age = 20 }; var s = $""{person.Name} is {person.Age} year(s) old""; -Console.Write(s);", build); +Console.Write(s);"); var result = await server.Run(new WorkspaceRequest(request)); @@ -115,10 +112,10 @@ public async Task Response_shows_program_output_when_compile_is_successful_and_s [Fact] public async Task When_compile_is_unsuccessful_then_no_exceptions_are_shown() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" -Console.WriteLine(banana);", build); +Console.WriteLine(banana);"); var result = await server.Run(new WorkspaceRequest(request)); result.Succeeded.Should().BeFalse(); @@ -128,10 +125,10 @@ public async Task When_compile_is_unsuccessful_then_no_exceptions_are_shown() [Fact] public async Task When_compile_is_unsuccessful_then_diagnostics_are_displayed_in_output() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" -Console.WriteLine(banana);", build); +Console.WriteLine(banana);"); var result = await server.Run(new WorkspaceRequest(request)); result.Succeeded.Should().BeFalse(); @@ -143,13 +140,13 @@ public async Task When_compile_is_unsuccessful_then_diagnostics_are_displayed_in [Fact] public async Task Multi_line_console_output_is_captured_correctly_a() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" Console.WriteLine(1); Console.WriteLine(2); Console.WriteLine(3); -Console.WriteLine(4);", build); +Console.WriteLine(4);"); var result = await server.Run(new WorkspaceRequest(request)); @@ -160,13 +157,13 @@ public async Task Multi_line_console_output_is_captured_correctly_a() [Fact] public async Task Multi_line_console_output_is_captured_correctly() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" Console.WriteLine(1); Console.WriteLine(2); Console.WriteLine(3); -Console.WriteLine(4);", build); +Console.WriteLine(4);"); var result = await server.Run(new WorkspaceRequest(request)); @@ -178,14 +175,14 @@ public async Task Multi_line_console_output_is_captured_correctly() public async Task Whitespace_is_preserved_in_multi_line_output() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); - + var server = GetCodeRunner(); + var request = CreateWorkspaceWithMainContaining(@" Console.WriteLine(); Console.WriteLine(1); Console.WriteLine(); Console.WriteLine(); -Console.WriteLine(2);", build); +Console.WriteLine(2);"); var result = await server.Run(new WorkspaceRequest(request)); @@ -195,14 +192,14 @@ public async Task Whitespace_is_preserved_in_multi_line_output() [Fact] public async Task Multi_line_console_output_is_captured_correctly_when_an_exception_is_thrown() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" Console.WriteLine(1); Console.WriteLine(2); throw new Exception(""oops!""); Console.WriteLine(3); -Console.WriteLine(4);", build); +Console.WriteLine(4);"); var timeBudget = new TimeBudget(10.Minutes()); @@ -217,9 +214,9 @@ public async Task Multi_line_console_output_is_captured_correctly_when_an_except [Fact] public async Task When_the_users_code_throws_on_first_line_then_it_is_returned_as_an_exception_property() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); - var request = CreateWorkspaceWithMainContaining(@"throw new Exception(""oops!"");", build); + var request = CreateWorkspaceWithMainContaining(@"throw new Exception(""oops!"");"); var result = await server.Run(new WorkspaceRequest(request)); @@ -230,10 +227,10 @@ public async Task When_the_users_code_throws_on_first_line_then_it_is_returned_a [Fact] public async Task When_the_users_code_throws_on_subsequent_line_then_it_is_returned_as_an_exception_property() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var request = CreateWorkspaceWithMainContaining(@" -throw new Exception(""oops!"");", build); +throw new Exception(""oops!"");"); var result = await server.Run(new WorkspaceRequest(request)); @@ -244,7 +241,7 @@ public async Task When_the_users_code_throws_on_subsequent_line_then_it_is_retur [Fact] public async Task When_a_public_void_Main_with_no_parameters_is_present_it_is_invoked() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var workspace = Workspace.FromSource(@" using System; @@ -255,7 +252,7 @@ public static void Main() { Console.WriteLine(""Hello there!""); } -}", workspaceType: build.Name); +}", workspaceType: "console"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -265,8 +262,8 @@ public static void Main() [Fact] public async Task When_a_public_void_Main_with_parameters_is_present_it_is_invoked() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); - + var server = GetCodeRunner(); + var workspace = Workspace.FromSource(@" using System; @@ -276,7 +273,7 @@ public static void Main(params string[] args) { Console.WriteLine(""Hello there!""); } -}", workspaceType: build.Name); +}", workspaceType: "console"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -286,7 +283,7 @@ public static void Main(params string[] args) [Fact] public async Task When_an_internal_void_Main_with_no_parameters_is_present_it_is_invoked() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var workspace = Workspace.FromSource(@" using System; @@ -297,7 +294,7 @@ static void Main() { Console.WriteLine(""Hello there!""); } -}", workspaceType: build.Name); +}", workspaceType:"console"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -309,7 +306,7 @@ static void Main() [Fact] public async Task When_an_internal_void_Main_with_parameters_is_present_it_is_invoked() { - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var workspace = Workspace.FromSource(@" using System; @@ -320,7 +317,7 @@ static void Main(string[] args) { Console.WriteLine(""Hello there!""); } -}", workspaceType: build.Name); +}", workspaceType: "console"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -334,7 +331,7 @@ public async Task Response_shows_warnings_with_successful_compilation() { var output = nameof(Response_shows_warnings_with_successful_compilation); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var workspace = CreateWorkspaceWithMainContaining($@" using System; @@ -347,7 +344,7 @@ public static void Main() var a = 0; Console.WriteLine(""{output}""); }} -}}", build); +}}"); var result = await server.Run(new WorkspaceRequest(workspace)); @@ -361,7 +358,7 @@ public async Task Response_shows_warnings_when_compilation_fails() { var output = nameof(Response_shows_warnings_when_compilation_fails); - var (server, build) = await GetRunnerAndWorkspaceBuild(); + var server = GetCodeRunner(); var workspace = CreateWorkspaceWithMainContaining($@" using System; @@ -373,7 +370,7 @@ public static void Main() var a = 0; Console.WriteLine(""{output}"") }} -}}", build); +}}"); var result = await server.Run(new WorkspaceRequest(workspace)); diff --git a/WorkspaceServer.Tests/WorkspaceServerTestsCore.cs b/WorkspaceServer.Tests/WorkspaceServerTestsCore.cs index 3eda61362..abfb493e3 100644 --- a/WorkspaceServer.Tests/WorkspaceServerTestsCore.cs +++ b/WorkspaceServer.Tests/WorkspaceServerTestsCore.cs @@ -2,11 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; using Pocket; -using WorkspaceServer.Packaging; using Xunit.Abstractions; namespace WorkspaceServer.Tests @@ -21,11 +17,11 @@ protected WorkspaceServerTestsCore(ITestOutputHelper output) } public void Dispose() => _disposables.Dispose(); - - protected abstract Task<(ICodeRunner runner, Package workspace)> GetRunnerAndWorkspaceBuild( - [CallerMemberName] string testName = null); - protected abstract ILanguageService GetLanguageService( - [CallerMemberName] string testName = null); + protected abstract ILanguageService GetLanguageService(); + + protected abstract ICodeCompiler GetCodeCompiler(); + + protected abstract ICodeRunner GetCodeRunner(); } } \ No newline at end of file diff --git a/WorkspaceServer/FileSystemDirectoryAccessor.cs b/WorkspaceServer/FileSystemDirectoryAccessor.cs index 48e2d01a9..f589c7fd2 100644 --- a/WorkspaceServer/FileSystemDirectoryAccessor.cs +++ b/WorkspaceServer/FileSystemDirectoryAccessor.cs @@ -5,7 +5,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Microsoft.DotNet.Try.Markdown; +using WorkspaceServer.Packaging; using WorkspaceServer.Servers.Roslyn; namespace WorkspaceServer @@ -75,6 +77,12 @@ public IDirectoryAccessor GetDirectoryAccessorForRelativePath(RelativeDirectoryP return new FileSystemDirectoryAccessor(new DirectoryInfo(absolutePath)); } + public Task TryLockAsync() + { + return FileLock.TryCreateAsync(this); + } + + public IEnumerable GetAllDirectoriesRecursively() { var directories = _rootDirectory.GetDirectories("*", SearchOption.AllDirectories); @@ -91,6 +99,14 @@ public IEnumerable GetAllFilesRecursively() new RelativeFilePath(PathUtilities.GetRelativePath(_rootDirectory.FullName, f.FullName))); } + public IEnumerable GetAllFiles() + { + var files = _rootDirectory.GetFiles("*", SearchOption.TopDirectoryOnly); + + return files.Select(f => + new RelativeFilePath(PathUtilities.GetRelativePath(_rootDirectory.FullName, f.FullName))); + } + public override string ToString() => _rootDirectory.FullName; } } diff --git a/WorkspaceServer/IDirectoryAccessor.cs b/WorkspaceServer/IDirectoryAccessor.cs index 4262f3e8b..cfbf76372 100644 --- a/WorkspaceServer/IDirectoryAccessor.cs +++ b/WorkspaceServer/IDirectoryAccessor.cs @@ -1,8 +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; using System.Collections.Generic; using System.IO; +using System.Threading.Tasks; using Microsoft.DotNet.Try.Markdown; namespace WorkspaceServer @@ -21,10 +23,14 @@ public interface IDirectoryAccessor IEnumerable GetAllFilesRecursively(); + IEnumerable GetAllFiles(); + IEnumerable GetAllDirectoriesRecursively(); FileSystemInfo GetFullyQualifiedPath(RelativePath path); IDirectoryAccessor GetDirectoryAccessorForRelativePath(RelativeDirectoryPath path); + + Task TryLockAsync(); } } \ No newline at end of file diff --git a/WorkspaceServer/PackageRegistry.cs b/WorkspaceServer/PackageRegistry.cs index 6fa0d0717..2ede19a40 100644 --- a/WorkspaceServer/PackageRegistry.cs +++ b/WorkspaceServer/PackageRegistry.cs @@ -9,12 +9,11 @@ using System.Linq; using System.Threading.Tasks; using Clockwise; -using Microsoft.CodeAnalysis.Operations; using WorkspaceServer.Packaging; namespace WorkspaceServer { - public class PackageRegistry : + public class PackageRegistry : IPackageFinder, IEnumerable> { @@ -54,7 +53,7 @@ private PackageRegistry( _strategies.Add(strategy); } - + _packageFinders = packageFinders?.ToList() ?? GetDefaultPackageFinders().ToList(); } @@ -88,9 +87,9 @@ public async Task Get(string packageName, Budget budget = null) // FIX: (Get) move this into the cache var package = await GetPackage2(descriptor); - if (package == null || !(package is T)) + if (!(package is T)) { - package = await GetPackageFromPackageBuilder(packageName, budget, descriptor); + package = await GetPackageFromPackageBuilder(packageName, budget, descriptor); } return (T)package; @@ -101,9 +100,21 @@ private Task GetPackage2(PackageDescriptor descriptor) { return _packages2.GetOrAdd(descriptor, async descriptor2 => { - foreach (var packgeFinder in _packageFinders) + foreach (var packageFinder in _packageFinders) { - if (await packgeFinder.Find(descriptor) is T pkg) + var package = await packageFinder.Find(descriptor); + if (package != null) + { + if (package is Package2 package2) + { + var packageAsset = package2.Assets.OfType().FirstOrDefault(); + if (packageAsset != null) + { + return packageAsset; + } + } + } + if (package is T pkg) { return pkg; } @@ -113,8 +124,7 @@ private Task GetPackage2(PackageDescriptor descriptor) }); } - private Task GetPackageFromPackageBuilder(string packageName, Budget budget, PackageDescriptor descriptor) - where T : IPackage + private Task GetPackageFromPackageBuilder(string packageName, Budget budget, PackageDescriptor descriptor) { return _packages.GetOrAdd(packageName, async name => { @@ -143,7 +153,7 @@ public static PackageRegistry CreateForTryMode(DirectoryInfo project, PackageSou { var finders = GetDefaultPackageFinders().Append(new WebAssemblyAssetFinder(Package.DefaultPackagesDirectory, addSource)); var registry = new PackageRegistry( - true, + true, addSource, finders, additionalStrategies: new LocalToolInstallingPackageDiscoveryStrategy(Package.DefaultPackagesDirectory, addSource)); @@ -214,7 +224,7 @@ public static PackageRegistry CreateForHostedMode() packageBuilder.AddPackageReference("Newtonsoft.Json"); packageBuilder.EnableBlazor(registry); }); - + registry.Add("blazor-ms.logging", packageBuilder => { diff --git a/WorkspaceServer/Packaging/FileLock.cs b/WorkspaceServer/Packaging/FileLock.cs new file mode 100644 index 000000000..10294295f --- /dev/null +++ b/WorkspaceServer/Packaging/FileLock.cs @@ -0,0 +1,48 @@ +// 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; + +namespace WorkspaceServer.Packaging +{ + public class FileLock + { + public static async Task TryCreateAsync(FileInfo lockFile) + { + if (lockFile == null) + { + throw new ArgumentNullException(nameof(lockFile)); + } + + const int waitAmount = 100; + var attemptCount = 1; + do + { + await Task.Delay(waitAmount * attemptCount); + attemptCount++; + + try + { + return File.Create(lockFile.FullName, 1, FileOptions.DeleteOnClose); + } + catch (IOException) + { + + } + } while (attemptCount <= 10); + + throw new IOException($"Cannot acquire file lock {lockFile.FullName}"); + } + + public static Task TryCreateAsync(IDirectoryAccessor directoryAccessor) + { + if (directoryAccessor == null) + { + throw new ArgumentNullException(nameof(directoryAccessor)); + } + return TryCreateAsync(directoryAccessor.GetFullyQualifiedFilePath(".trydotnet-lock")); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Packaging/IPackage.cs b/WorkspaceServer/Packaging/IPackage.cs index aef995a0a..e788ef398 100644 --- a/WorkspaceServer/Packaging/IPackage.cs +++ b/WorkspaceServer/Packaging/IPackage.cs @@ -1,7 +1,6 @@ // 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 Clockwise; @@ -29,12 +28,17 @@ public interface IMightSupportBlazor : IPackage bool CanSupportBlazor { get; } } - public interface ICreateWorkspaceForRun : IPackage + public interface ICreateWorkspace : IPackage + { + Task CreateRoslynWorkspaceAsync(Budget budget); + } + + public interface ICreateWorkspaceForRun : IPackage, ICreateWorkspace { Task CreateRoslynWorkspaceForRunAsync(Budget budget); } - public interface ICreateWorkspaceForLanguageServices : IPackage + public interface ICreateWorkspaceForLanguageServices : IPackage, ICreateWorkspace { Task CreateRoslynWorkspaceForLanguageServicesAsync(Budget budget); } diff --git a/WorkspaceServer/Packaging/Package.cs b/WorkspaceServer/Packaging/Package.cs index 9117b0a12..c96dfb0e6 100644 --- a/WorkspaceServer/Packaging/Package.cs +++ b/WorkspaceServer/Packaging/Package.cs @@ -35,7 +35,7 @@ public abstract class Package : ICreateWorkspaceForRun { internal const string DesignTimeBuildBinlogFileName = "package_designTimeBuild.binlog"; - private static readonly object _createDirectoryLock = new object(); + private static readonly ConcurrentDictionary _packageBuildSemaphores = new ConcurrentDictionary(); private static readonly ConcurrentDictionary _packagePublishSemaphores = new ConcurrentDictionary(); @@ -515,91 +515,16 @@ protected async Task Publish() } } - public static async Task Copy( - Package fromPackage, - string folderNameStartsWith = null, - bool isRebuildable = false, - IScheduler buildThrottleScheduler = null, - DirectoryInfo parentDirectory = null) - { - if (fromPackage == null) - { - throw new ArgumentNullException(nameof(fromPackage)); - } - - await fromPackage.EnsureReady(new Budget()); - - folderNameStartsWith = folderNameStartsWith ?? fromPackage.Name; - parentDirectory = parentDirectory ?? fromPackage.Directory.Parent; - - var destination = - CreateDirectory(folderNameStartsWith, - parentDirectory); - - fromPackage.Directory.CopyTo(destination); - - var binLogs = destination.GetFiles("*.binlog"); - - foreach (var fileInfo in binLogs) - { - fileInfo.Delete(); - } - - Package copy; - if (isRebuildable) - { - copy = new RebuildablePackage(directory: destination, name: destination.Name, buildThrottleScheduler: buildThrottleScheduler) - { - IsDirectoryCreated = true - }; - } - else - { - copy = new NonrebuildablePackage(directory: destination, name: destination.Name, buildThrottleScheduler: buildThrottleScheduler) - { - IsDirectoryCreated = true - }; - } - - Log.Info( - "Copied workspace {from} to {to}", - fromPackage, - copy); + - return copy; - } - - public static DirectoryInfo CreateDirectory( - string folderNameStartsWith, - DirectoryInfo parentDirectory = null) + public override string ToString() { - if (string.IsNullOrWhiteSpace(folderNameStartsWith)) - { - throw new ArgumentException("Value cannot be null or whitespace.", nameof(folderNameStartsWith)); - } - - parentDirectory = parentDirectory ?? DefaultPackagesDirectory; - - DirectoryInfo created; - - lock (_createDirectoryLock) - { - if (!parentDirectory.Exists) - { - parentDirectory.Create(); - } - - var existingFolders = parentDirectory.GetDirectories($"{folderNameStartsWith}.*"); - - created = parentDirectory.CreateSubdirectory($"{folderNameStartsWith}.{existingFolders.Length + 1}"); - } - - return created; + return $"{Name} ({Directory.FullName})"; } - public override string ToString() + public Task CreateRoslynWorkspaceAsync(Budget budget) { - return $"{Name} ({Directory.FullName})"; + return CreateRoslynWorkspaceForRunAsync(budget); } protected SyntaxTree CreateInstrumentationEmitterSyntaxTree() diff --git a/WorkspaceServer/Packaging/Package2.cs b/WorkspaceServer/Packaging/Package2.cs index ca21909da..742a1f984 100644 --- a/WorkspaceServer/Packaging/Package2.cs +++ b/WorkspaceServer/Packaging/Package2.cs @@ -9,8 +9,7 @@ namespace WorkspaceServer.Packaging { - public class Package2 : - IPackage, + public class Package2 : IHaveADirectory, IHaveADirectoryAccessor, IMightSupportBlazor diff --git a/WorkspaceServer/Packaging/PackageAsset.cs b/WorkspaceServer/Packaging/PackageAsset.cs index 530548d43..9b29afbd9 100644 --- a/WorkspaceServer/Packaging/PackageAsset.cs +++ b/WorkspaceServer/Packaging/PackageAsset.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.IO; namespace WorkspaceServer.Packaging { @@ -16,14 +15,6 @@ protected PackageAsset(IDirectoryAccessor directoryAccessor) public IDirectoryAccessor DirectoryAccessor { get; } } - public class ProjectAsset : PackageAsset - { - - public ProjectAsset(IDirectoryAccessor directoryAccessor) : base(directoryAccessor) - { - } - } - public class WebAssemblyAsset : PackageAsset { public WebAssemblyAsset(IDirectoryAccessor directoryAccessor) : base(directoryAccessor) diff --git a/WorkspaceServer/Packaging/PackageBase.cs b/WorkspaceServer/Packaging/PackageBase.cs index 14580a1d3..2f8379a00 100644 --- a/WorkspaceServer/Packaging/PackageBase.cs +++ b/WorkspaceServer/Packaging/PackageBase.cs @@ -4,7 +4,6 @@ using System; using System.IO; using Clockwise; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using MLS.Agent.Tools; @@ -15,10 +14,9 @@ namespace WorkspaceServer.Packaging { public abstract class PackageBase : - IPackage, IHaveADirectory, - IMightSupportBlazor - , IHaveADirectoryAccessor + IMightSupportBlazor, + IHaveADirectoryAccessor { IDirectoryAccessor IHaveADirectoryAccessor.Directory => new FileSystemDirectoryAccessor(Directory); @@ -46,8 +44,7 @@ protected PackageBase( public string Name { get; } public DirectoryInfo Directory { get; set; } - - protected bool IsDirectoryCreated { get; set; } + protected Task EnsureCreated() => _lazyCreation.ValueAsync(); diff --git a/WorkspaceServer/Packaging/PackageBuilder.cs b/WorkspaceServer/Packaging/PackageBuilder.cs index 1889d033c..1d7eebc49 100644 --- a/WorkspaceServer/Packaging/PackageBuilder.cs +++ b/WorkspaceServer/Packaging/PackageBuilder.cs @@ -33,7 +33,7 @@ public PackageBuilder(string packageName, IPackageInitializer packageInitializer public DirectoryInfo Directory { get; set; } - public bool CreateRebuildablePackage { get; internal set; } + public bool CreateRebuildablePackage { get; set; } public bool BlazorSupported { get; private set; } diff --git a/WorkspaceServer/Packaging/PipelineStep.cs b/WorkspaceServer/Packaging/PipelineStep.cs new file mode 100644 index 000000000..f954bd2ee --- /dev/null +++ b/WorkspaceServer/Packaging/PipelineStep.cs @@ -0,0 +1,124 @@ +// 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.Threading.Tasks; + +namespace WorkspaceServer.Packaging +{ + public interface IPipelineStep + { + void Invalidate(); + } + + public class PipelineStep : IPipelineStep + { + private readonly Func> _createValue; + private TaskCompletionSource _latestValue = new TaskCompletionSource(); + private Task _inFlight; + private readonly object _lock = new object(); + private bool _invalidated = true; + private Guid _operationId; + private IPipelineStep _nextStep; + + public PipelineStep(Func> createValue) + { + if (createValue == null) throw new ArgumentNullException(nameof(createValue)); + + _createValue = async () => + { + await Task.Yield(); + return await createValue(); + }; + } + + public PipelineStep Then(Func> nextStep) + { + var newStep= new PipelineStep(async () => + { + var previousStepValue = await GetLatestAsync(); + return await nextStep(previousStepValue); + }); + + _nextStep = newStep; + return newStep; + } + + + + public Task GetLatestAsync() + { + lock (_lock) + { + if (_invalidated) + { + if (_inFlight == null) + { + _latestValue = new TaskCompletionSource(); + _inFlight = _createValue(); + } + else + { + _inFlight = _inFlight + .ContinueWith(t => _createValue().Result); + } + var newId = Guid.NewGuid(); + _operationId = newId; + _inFlight.ContinueWith(t => + { + lock (_lock) + { + switch (t.Status) + { + case TaskStatus.Faulted: + + if (_operationId == newId) + { + _latestValue.SetException(t.Exception.InnerException); + } + + _inFlight = null; + _invalidated = true; + + break; + case TaskStatus.RanToCompletion: + + if (_operationId == newId) + { + _latestValue.SetResult(t.Result); + } + + _inFlight = null; + _invalidated = false; + + break; + } + } + }); + _invalidated = false; + return _latestValue.Task; + + } + else + { + return _latestValue.Task; + } + } + + } + + public void Invalidate() + { + lock (_lock) + { + if (!_invalidated) + { + _invalidated = true; + + } + } + + _nextStep?.Invalidate(); + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Packaging/ProjectAsset.cs b/WorkspaceServer/Packaging/ProjectAsset.cs new file mode 100644 index 000000000..aeafaae87 --- /dev/null +++ b/WorkspaceServer/Packaging/ProjectAsset.cs @@ -0,0 +1,226 @@ +// 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.Linq; +using System.Threading.Tasks; +using Buildalyzer; +using Clockwise; +using Microsoft.CodeAnalysis; +using MLS.Agent.Tools; +using Pocket; +using WorkspaceServer.Servers.Roslyn; +using static Pocket.Logger; + +namespace WorkspaceServer.Packaging +{ + public class ProjectAsset : PackageAsset, + ICreateWorkspaceForLanguageServices, + ICreateWorkspaceForRun, + IHaveADirectory + { + private const string FullBuildBinlogFileName = "package_fullBuild.binlog"; + private readonly FileInfo _projectFile; + private readonly FileInfo _lastBuildErrorLogFile; + private readonly PipelineStep _projectBuildStep; + private readonly PipelineStep _workspaceStep; + private readonly PipelineStep _cleanupStep; + + public string Name { get; } + + public DirectoryInfo Directory { get; } + + public ProjectAsset(IDirectoryAccessor directoryAccessor, string csprojFileName = null) : base(directoryAccessor) + { + if (directoryAccessor == null) + { + throw new ArgumentNullException(nameof(directoryAccessor)); + } + + if (string.IsNullOrWhiteSpace(csprojFileName)) + { + var firstProject = DirectoryAccessor.GetAllFiles().Single(f => f.Extension == ".csproj"); + _projectFile = DirectoryAccessor.GetFullyQualifiedFilePath(firstProject.FileName); + } + else + { + _projectFile = DirectoryAccessor.GetFullyQualifiedFilePath(csprojFileName); + } + + Directory = DirectoryAccessor.GetFullyQualifiedRoot(); + Name = _projectFile?.Name ?? Directory?.Name; + _lastBuildErrorLogFile = directoryAccessor.GetFullyQualifiedFilePath(".trydotnet-builderror"); + _cleanupStep = new PipelineStep(LoadResultOrCleanAsync); + _projectBuildStep = _cleanupStep.Then(BuildProjectAsync); + _workspaceStep = _projectBuildStep.Then(BuildWorkspaceAsync); + } + + private async Task LoadResultOrCleanAsync() + { + using (await DirectoryAccessor.TryLockAsync()) + { + var binLog = this.FindLatestBinLog(); + if (binLog != null) + { + var results = await TryLoadAnalyzerResultsAsync(binLog); + var result = results?.FirstOrDefault(p => p.ProjectFilePath == _projectFile.FullName); + + var didCompile = DidPerformCoreCompile(result); + if (result != null) + { + if (result.Succeeded && didCompile) + { + return result; + } + } + } + + binLog?.DoWhenFileAvailable(() => binLog.Delete()); + var toClean = Directory.GetDirectories("obj"); + foreach (var directoryInfo in toClean) + { + directoryInfo.Delete(true); + } + + return null; + } + } + + private bool DidPerformCoreCompile(AnalyzerResult result) + { + if (result == null) + { + return false; + } + + var sourceCount = result.SourceFiles?.Length ?? 0; + var compilerInputs = result.GetCompileInputs()?.Length ?? 0; + + return compilerInputs > 0 && sourceCount > 0; + } + + private Task BuildWorkspaceAsync(AnalyzerResult result) + { + if (result.TryGetWorkspace(out var ws)) + { + var projectId = ws.CurrentSolution.ProjectIds.FirstOrDefault(); + var references = result.References; + var metadataReferences = references.GetMetadataReferences(); + var solution = ws.CurrentSolution; + solution = solution.WithProjectMetadataReferences(projectId, metadataReferences); + ws.TryApplyChanges(solution); + return Task.FromResult(ws); + } + throw new InvalidOperationException("Failed creating workspace"); + } + + private async Task BuildProjectAsync(AnalyzerResult result) + { + if (result != null) + { + return result; + } + + using (await DirectoryAccessor.TryLockAsync()) + { + + using (var operation = Log.OnEnterAndConfirmOnExit()) + { + try + { + operation.Info("Attempting building package {name}", Name); + await DotnetBuild(); + operation.Info("Workspace built"); + operation.Succeed(); + } + catch (Exception exception) + { + operation.Error("Exception building workspace", exception); + throw; + } + + var binLog = this.FindLatestBinLog(); + + if (binLog == null) + { + throw new InvalidOperationException("Failed to build"); + } + + var results = await TryLoadAnalyzerResultsAsync(binLog); + + if (results?.Count == 0) + { + throw new InvalidOperationException("The build log seems to contain no solutions or projects"); + } + + result = results?.FirstOrDefault(p => p.ProjectFilePath == _projectFile.FullName); + + if (result?.Succeeded == true) + { + return result; + } + + throw new InvalidOperationException("Failed to build"); + } + } + } + + private async Task TryLoadAnalyzerResultsAsync(FileInfo binLog) + { + AnalyzerResults results = null; + await binLog.DoWhenFileAvailable(() => + { + var manager = new AnalyzerManager(); + results = manager.Analyze(binLog.FullName); + }); + return results; + } + + public Task CreateRoslynWorkspaceAsync(Budget budget) + { + return _workspaceStep.GetLatestAsync(); + } + + public Task CreateRoslynWorkspaceForRunAsync(Budget budget) + { + return CreateRoslynWorkspaceAsync(budget); + } + + public Task CreateRoslynWorkspaceForLanguageServicesAsync(Budget budget) + { + return CreateRoslynWorkspaceAsync(budget); + } + + + protected async Task DotnetBuild() + { + using (var operation = Log.OnEnterAndConfirmOnExit()) + { + var args = $"/bl:{FullBuildBinlogFileName}"; + if (_projectFile?.Exists == true) + { + args = $@"""{_projectFile.FullName}"" {args}"; + } + + operation.Info("Building package {name} in {directory}", Name, Directory); + + var result = await new Dotnet(Directory).Build(args: args); + + if (result.ExitCode != 0) + { + File.WriteAllText( + _lastBuildErrorLogFile.FullName, + string.Join(Environment.NewLine, result.Error)); + } + else if (_lastBuildErrorLogFile.Exists) + { + _lastBuildErrorLogFile.Delete(); + } + + result.ThrowOnFailure(); + operation.Succeed(); + } + } + } +} \ No newline at end of file diff --git a/WorkspaceServer/Packaging/ProjectAssetLoader.cs b/WorkspaceServer/Packaging/ProjectAssetLoader.cs index 6e464dda5..6ec247907 100644 --- a/WorkspaceServer/Packaging/ProjectAssetLoader.cs +++ b/WorkspaceServer/Packaging/ProjectAssetLoader.cs @@ -18,7 +18,7 @@ public Task> LoadAsync(Package2 package) foreach (var csproj in directory.GetAllFilesRecursively() .Where(f => f.Extension == ".csproj")) { - assets.Add(new ProjectAsset(directory.GetDirectoryAccessorForRelativePath(csproj.Directory))); + assets.Add(new ProjectAsset(directory.GetDirectoryAccessorForRelativePath(csproj.Directory), csproj.FileName)); } return diff --git a/WorkspaceServer/Servers/Roslyn/CompilationUtility.cs b/WorkspaceServer/Servers/Roslyn/CompilationUtility.cs index bee272fdd..ce7fa62ee 100644 --- a/WorkspaceServer/Servers/Roslyn/CompilationUtility.cs +++ b/WorkspaceServer/Servers/Roslyn/CompilationUtility.cs @@ -1,6 +1,7 @@ // 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.Generic; using System.IO; using Buildalyzer.Workspaces; @@ -38,6 +39,11 @@ public static IEnumerable FindBinLogs(this IHaveADirectory package) => public static async Task WaitForFileAvailable( this FileInfo file) { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + const int waitAmount = 100; var attemptCount = 1; while (file.Exists && attemptCount <= 10 && !IsAvailable()) @@ -62,6 +68,35 @@ bool IsAvailable() } } + public static async Task DoWhenFileAvailable( + this FileInfo file, Action action) + { + if (file == null) + { + throw new ArgumentNullException(nameof(file)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + const int waitAmount = 100; + var attemptCount = 1; + while (file.Exists && attemptCount <= 10) + { + try + { + action(); + break; + } + catch(IOException){ + await Task.Delay(waitAmount * attemptCount); + attemptCount++; + } + } + } + public static FileInfo GetProjectFile(this IHaveADirectory packageBase) => packageBase.Directory.GetFiles("*.csproj").FirstOrDefault(); @@ -105,12 +140,7 @@ internal static string GetTargetFramework(this IHaveADirectory ihad) .GetFiles("*.runtimeconfig.json", SearchOption.AllDirectories) .FirstOrDefault(); - if (runtimeConfig != null) - { - return RuntimeConfig.GetTargetFramework(runtimeConfig); - } - - return "netstandard2.0"; + return runtimeConfig != null ? RuntimeConfig.GetTargetFramework(runtimeConfig) : "netstandard2.0"; } } } \ No newline at end of file diff --git a/WorkspaceServer/Servers/Roslyn/PackageExtensions.cs b/WorkspaceServer/Servers/Roslyn/PackageExtensions.cs index dbcbf8504..2c496b45c 100644 --- a/WorkspaceServer/Servers/Roslyn/PackageExtensions.cs +++ b/WorkspaceServer/Servers/Roslyn/PackageExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.DotNet.Try.Project; using Microsoft.DotNet.Try.Protocol; +using WorkspaceServer.Packaging; using WorkspaceServer.Servers.Roslyn.Instrumentation; using static System.Environment; using Package = WorkspaceServer.Packaging.Package; @@ -162,17 +163,64 @@ await InstrumentationLineMapper.MapLineLocationsRelativeToViewportAsync( package.GetCompilation(sources, sourceCodeKind, defaultUsings, () => package.CreateRoslynWorkspaceForRunAsync(budget), budget); public static Task<(Compilation compilation, IReadOnlyCollection documents)> GetCompilationForLanguageServices( - this Package package, + this ICreateWorkspace package, IReadOnlyCollection sources, SourceCodeKind sourceCodeKind, IEnumerable defaultUsings, Budget budget) => - package.GetCompilation(sources, sourceCodeKind, defaultUsings, () => package.CreateRoslynWorkspaceForLanguageServicesAsync(budget), budget); + package.GetCompilation(sources, sourceCodeKind, defaultUsings, () => package.CreateRoslynWorkspaceAsync(budget), budget); private static Document GetActiveDocument(IEnumerable documents, BufferId activeBufferId) { return documents.First(d => d.Name.Equals(activeBufferId.FileName)); } + + public static async Task<(Compilation compilation, IReadOnlyCollection documents)> GetCompilation( + this IPackage package, + IReadOnlyCollection sources, + SourceCodeKind sourceCodeKind, + IEnumerable defaultUsings, + Func> workspaceFactory, + Budget budget) + { + var workspace = await workspaceFactory(); + + var currentSolution = workspace.CurrentSolution; + var project = currentSolution.Projects.First(); + var projectId = project.Id; + foreach (var source in sources) + { + if (currentSolution.Projects + .SelectMany(p => p.Documents) + .FirstOrDefault(d => d.IsMatch(source)) is Document document) + { + // there's a pre-existing document, so overwrite its contents + document = document.WithText(source.Text); + document = document.WithSourceCodeKind(sourceCodeKind); + currentSolution = document.Project.Solution; + } + else + { + var docId = DocumentId.CreateNewId(projectId, $"{package.Name}.Document"); + + currentSolution = currentSolution.AddDocument(docId, source.Name, source.Text); + currentSolution = currentSolution.WithDocumentSourceCodeKind(docId, sourceCodeKind); + } + } + + + project = currentSolution.GetProject(projectId); + var usings = defaultUsings?.ToArray() ?? Array.Empty(); + if (usings.Length > 0) + { + var options = (CSharpCompilationOptions)project.CompilationOptions; + project = project.WithCompilationOptions(options.WithUsings(usings)); + } + + var compilation = await project.GetCompilationAsync().CancelIfExceeds(budget); + + return (compilation, project.Documents.ToArray()); + } } } diff --git a/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs b/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs index f0dc3c292..3b25661d2 100644 --- a/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs +++ b/WorkspaceServer/Servers/Roslyn/RoslynWorkspaceServer.cs @@ -32,7 +32,7 @@ public class RoslynWorkspaceServer : ILanguageService, ICodeRunner, ICodeCompile { private readonly IPackageFinder _packageFinder; private const int defaultBudgetInSeconds = 30; - private readonly ConcurrentDictionary locks = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary locks = new ConcurrentDictionary(); private readonly IWorkspaceTransformer _transformer = new BufferInliningTransformer(); private static readonly string UserCodeCompleted = nameof(UserCodeCompleted); @@ -49,7 +49,7 @@ public RoslynWorkspaceServer(IPackageFinder packageRegistry) public async Task GetCompletionList(WorkspaceRequest request, Budget budget) { budget = budget ?? new TimeBudget(TimeSpan.FromSeconds(defaultBudgetInSeconds)); - var package = await _packageFinder.Find(request.Workspace.WorkspaceType); + var package = await _packageFinder.Find(request.Workspace.WorkspaceType); var processed = await _transformer.TransformAsync(request.Workspace); var sourceFiles = processed.GetSourceFiles(); @@ -119,7 +119,7 @@ public async Task GetSignatureHelp(WorkspaceRequest request { budget = budget ?? new TimeBudget(TimeSpan.FromSeconds(defaultBudgetInSeconds)); - var package = await _packageFinder.Find(request.Workspace.WorkspaceType); + var package = await _packageFinder.Find(request.Workspace.WorkspaceType); var processed = await _transformer.TransformAsync(request.Workspace); @@ -160,7 +160,7 @@ public async Task GetDiagnostics(WorkspaceRequest request, Bud { budget = budget ?? new TimeBudget(TimeSpan.FromSeconds(defaultBudgetInSeconds)); - var package = await _packageFinder.Find(request.Workspace.WorkspaceType); + var package = await _packageFinder.Find(request.Workspace.WorkspaceType); var workspace = await _transformer.TransformAsync(request.Workspace);