diff --git a/src/Workspaces/MSBuildWorkspaceLoader.cs b/src/Workspaces/MSBuildWorkspaceLoader.cs index fa91d1124b..97f0f7f118 100644 --- a/src/Workspaces/MSBuildWorkspaceLoader.cs +++ b/src/Workspaces/MSBuildWorkspaceLoader.cs @@ -13,7 +13,8 @@ namespace Microsoft.CodeAnalysis.Tools.Workspaces { internal static class MSBuildWorkspaceLoader { - private static readonly SemaphoreSlim s_guard = new SemaphoreSlim(1, 1); + // Used in tests for locking around MSBuild invocations + internal static readonly SemaphoreSlim Guard = new SemaphoreSlim(1, 1); public static async Task LoadAsync( string solutionOrProjectPath, @@ -31,45 +32,35 @@ internal static class MSBuildWorkspaceLoader { "AlwaysCompileMarkupFilesInSeparateDomain", bool.FalseString }, }; - MSBuildWorkspace workspace; + var workspace = MSBuildWorkspace.Create(properties); - await s_guard.WaitAsync(); - try + Build.Framework.ILogger? binlog = null; + if (createBinaryLog) { - workspace = MSBuildWorkspace.Create(properties); - - Build.Framework.ILogger? binlog = null; - if (createBinaryLog) + binlog = new Build.Logging.BinaryLogger() { - binlog = new Build.Logging.BinaryLogger() - { - Parameters = Path.Combine(Environment.CurrentDirectory, "formatDiagnosticLog.binlog"), - Verbosity = Build.Framework.LoggerVerbosity.Diagnostic, - }; - } + Parameters = Path.Combine(Environment.CurrentDirectory, "formatDiagnosticLog.binlog"), + Verbosity = Build.Framework.LoggerVerbosity.Diagnostic, + }; + } - if (workspaceType == WorkspaceType.Solution) + if (workspaceType == WorkspaceType.Solution) + { + await workspace.OpenSolutionAsync(solutionOrProjectPath, msbuildLogger: binlog, cancellationToken: cancellationToken).ConfigureAwait(false); + } + else + { + try { - await workspace.OpenSolutionAsync(solutionOrProjectPath, msbuildLogger: binlog, cancellationToken: cancellationToken).ConfigureAwait(false); + await workspace.OpenProjectAsync(solutionOrProjectPath, msbuildLogger: binlog, cancellationToken: cancellationToken).ConfigureAwait(false); } - else + catch (InvalidOperationException) { - try - { - await workspace.OpenProjectAsync(solutionOrProjectPath, msbuildLogger: binlog, cancellationToken: cancellationToken).ConfigureAwait(false); - } - catch (InvalidOperationException) - { - logger.LogError(Resources.Could_not_format_0_Format_currently_supports_only_CSharp_and_Visual_Basic_projects, solutionOrProjectPath); - workspace.Dispose(); - return null; - } + logger.LogError(Resources.Could_not_format_0_Format_currently_supports_only_CSharp_and_Visual_Basic_projects, solutionOrProjectPath); + workspace.Dispose(); + return null; } } - finally - { - s_guard.Release(); - } LogWorkspaceDiagnostics(logger, logWorkspaceWarnings, workspace.Diagnostics); @@ -100,5 +91,27 @@ static void LogWorkspaceDiagnostics(ILogger logger, bool logWorkspaceWarnings, I } } } + + /// + /// This is for use in tests so that MSBuild is only invoked serially + /// + internal static async Task LockedLoadAsync( + string solutionOrProjectPath, + WorkspaceType workspaceType, + bool createBinaryLog, + bool logWorkspaceWarnings, + ILogger logger, + CancellationToken cancellationToken) + { + await Guard.WaitAsync(); + try + { + return await LoadAsync(solutionOrProjectPath, workspaceType, createBinaryLog, logWorkspaceWarnings, logger, cancellationToken); + } + finally + { + Guard.Release(); + } + } } } diff --git a/tests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs b/tests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs index 7893f46d03..8fd7e5a154 100644 --- a/tests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs +++ b/tests/Analyzers/ThirdPartyAnalyzerFormatterTests.cs @@ -43,7 +43,7 @@ public async Task InitializeAsync() var workspacePath = Path.Combine(TestProjectsPathHelper.GetProjectsDirectory(), s_analyzerProjectFilePath); MSBuildRegistrar.RegisterInstance(logger); - var analyzerWorkspace = await MSBuildWorkspaceLoader.LoadAsync(workspacePath, WorkspaceType.Project, createBinaryLog: false, logWorkspaceWarnings: true, logger, CancellationToken.None); + var analyzerWorkspace = await MSBuildWorkspaceLoader.LockedLoadAsync(workspacePath, WorkspaceType.Project, createBinaryLog: false, logWorkspaceWarnings: true, logger, CancellationToken.None); // From this project we can get valid AnalyzerReferences to add to our test project. _analyzerReferencesProject = analyzerWorkspace.CurrentSolution.Projects.Single(); diff --git a/tests/CodeFormatterTests.cs b/tests/CodeFormatterTests.cs index 5964ced988..814f6cb274 100644 --- a/tests/CodeFormatterTests.cs +++ b/tests/CodeFormatterTests.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Tools.Tests.Utilities; +using Microsoft.CodeAnalysis.Tools.Tests.XUnit; using Microsoft.CodeAnalysis.Tools.Utilities; using Microsoft.Extensions.Logging; using Xunit; @@ -48,7 +49,7 @@ public CodeFormatterTests(ITestOutputHelper output) _output = output; } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInFormattedProject() { await TestFormatWorkspaceAsync( @@ -61,7 +62,7 @@ public async Task NoFilesFormattedInFormattedProject() expectedFileCount: 3); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInFormattedSolution() { await TestFormatWorkspaceAsync( @@ -74,7 +75,7 @@ public async Task NoFilesFormattedInFormattedSolution() expectedFileCount: 3); } - [Fact] + [MSBuildFact] public async Task FilesFormattedInUnformattedProject() { await TestFormatWorkspaceAsync( @@ -87,7 +88,7 @@ public async Task FilesFormattedInUnformattedProject() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInUnformattedProjectWhenFixingCodeStyle() { await TestFormatWorkspaceAsync( @@ -102,7 +103,7 @@ public async Task NoFilesFormattedInUnformattedProjectWhenFixingCodeStyle() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task GeneratedFilesFormattedInUnformattedProject() { var log = await TestFormatWorkspaceAsync( @@ -119,7 +120,7 @@ public async Task GeneratedFilesFormattedInUnformattedProject() Assert.Contains(logLines, line => line.Contains("NETCoreApp,Version=v3.0.AssemblyAttributes.cs")); } - [Fact] + [MSBuildFact] public async Task FilesFormattedInUnformattedSolution() { await TestFormatWorkspaceAsync( @@ -132,7 +133,7 @@ public async Task FilesFormattedInUnformattedSolution() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task FilesFormattedInUnformattedProjectFolder() { // Since the code files are beneath the project folder, files are found and formatted. @@ -146,7 +147,7 @@ public async Task FilesFormattedInUnformattedProjectFolder() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInUnformattedSolutionFolder() { // Since the code files are outside the solution folder, no files are found or formatted. @@ -160,7 +161,7 @@ public async Task NoFilesFormattedInUnformattedSolutionFolder() expectedFileCount: 0); } - [Fact] + [MSBuildFact] public async Task FSharpProjectsDoNotCreateException() { var log = await TestFormatWorkspaceAsync( @@ -179,7 +180,7 @@ public async Task FSharpProjectsDoNotCreateException() Assert.EndsWith(s_fSharpProjectFilePath, match.Groups[1].Value); } - [Fact] + [MSBuildFact] public async Task OnlyFormatPathsFromList() { // To match a folder pattern it needs to end with a directory separator. @@ -195,7 +196,7 @@ public async Task OnlyFormatPathsFromList() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task OnlyFormatFilesFromList() { var include = new[] { s_unformattedProgramFilePath }; @@ -210,7 +211,7 @@ public async Task OnlyFormatFilesFromList() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedWhenNotInList() { var include = new[] { Path.Combine(s_unformattedProjectPath, "does_not_exist.cs") }; @@ -225,7 +226,7 @@ public async Task NoFilesFormattedWhenNotInList() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task OnlyLogFormattedFiles() { var include = new[] { s_unformattedProgramFilePath }; @@ -246,7 +247,7 @@ public async Task OnlyLogFormattedFiles() Assert.EndsWith("Program.cs", match.Groups[1].Value); } - [Fact] + [MSBuildFact] public async Task FormatLocationsLoggedInUnformattedProject() { var log = await TestFormatWorkspaceAsync( @@ -297,7 +298,7 @@ public async Task FormatLocationsLoggedInUnformattedProject() } } - [Fact] + [MSBuildFact] public async Task FormatLocationsNotLoggedInFormattedProject() { var log = await TestFormatWorkspaceAsync( @@ -315,7 +316,7 @@ public async Task FormatLocationsNotLoggedInFormattedProject() Assert.Empty(formatLocations); } - [Fact] + [MSBuildFact] public async Task LogFilesThatDontMatchExclude() { var include = new[] { s_unformattedProgramFilePath }; @@ -336,7 +337,7 @@ public async Task LogFilesThatDontMatchExclude() Assert.EndsWith("Program.cs", match.Groups[1].Value); } - [Fact] + [MSBuildFact] public async Task IgnoreFileWhenListedInExcludeList() { var include = new[] { s_unformattedProgramFilePath }; @@ -351,7 +352,7 @@ public async Task IgnoreFileWhenListedInExcludeList() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task IgnoreFileWhenContainingFolderListedInExcludeList() { var include = new[] { s_unformattedProgramFilePath }; @@ -367,7 +368,7 @@ public async Task IgnoreFileWhenContainingFolderListedInExcludeList() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task IgnoreAllFileWhenExcludingAllFiles() { var include = new[] { s_unformattedProgramFilePath }; @@ -383,7 +384,7 @@ public async Task IgnoreAllFileWhenExcludingAllFiles() expectedFileCount: 6); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInGeneratedProject_WhenNotIncludingGeneratedCode() { await TestFormatWorkspaceAsync( @@ -396,7 +397,7 @@ public async Task NoFilesFormattedInGeneratedProject_WhenNotIncludingGeneratedCo expectedFileCount: 3); } - [Fact] + [MSBuildFact] public async Task FilesFormattedInGeneratedProject_WhenIncludingGeneratedCode() { await TestFormatWorkspaceAsync( @@ -409,7 +410,7 @@ public async Task FilesFormattedInGeneratedProject_WhenIncludingGeneratedCode() expectedFileCount: 3); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInCodeStyleSolution_WhenNotFixingCodeStyle() { var restoreExitCode = await NuGetHelper.PerformRestore(s_codeStyleSolutionFilePath, _output); @@ -426,7 +427,7 @@ public async Task NoFilesFormattedInCodeStyleSolution_WhenNotFixingCodeStyle() fixCategory: FixCategory.Whitespace | FixCategory.CodeStyle); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInCodeStyleSolution_WhenFixingCodeStyleErrors() { var restoreExitCode = await NuGetHelper.PerformRestore(s_codeStyleSolutionFilePath, _output); @@ -444,7 +445,7 @@ public async Task NoFilesFormattedInCodeStyleSolution_WhenFixingCodeStyleErrors( codeStyleSeverity: DiagnosticSeverity.Error); } - [Fact] + [MSBuildFact] public async Task FilesFormattedInCodeStyleSolution_WhenFixingCodeStyleWarnings() { var restoreExitCode = await NuGetHelper.PerformRestore(s_codeStyleSolutionFilePath, _output); @@ -462,7 +463,7 @@ public async Task FilesFormattedInCodeStyleSolution_WhenFixingCodeStyleWarnings( codeStyleSeverity: DiagnosticSeverity.Warning); } - [Fact] + [MSBuildFact] public async Task NoFilesFormattedInAnalyzersSolution_WhenNotFixingAnalyzers() { var restoreExitCode = await NuGetHelper.PerformRestore(s_analyzersSolutionFilePath, _output); @@ -479,7 +480,7 @@ public async Task NoFilesFormattedInAnalyzersSolution_WhenNotFixingAnalyzers() fixCategory: FixCategory.Whitespace); } - [Fact] + [MSBuildFact] public async Task FilesFormattedInAnalyzersSolution_WhenFixingAnalyzerErrors() { var restoreExitCode = await NuGetHelper.PerformRestore(s_analyzersSolutionFilePath, _output); diff --git a/tests/XUnit/MSBuildFactAttribute.cs b/tests/XUnit/MSBuildFactAttribute.cs new file mode 100644 index 0000000000..cbdc58737e --- /dev/null +++ b/tests/XUnit/MSBuildFactAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using Xunit; +using Xunit.Sdk; + +#nullable enable + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("Microsoft.CodeAnalysis.Tools.Tests.XUnit.MSBuildFactDiscoverer", "dotnet-format.UnitTests")] + public sealed class MSBuildFactAttribute : FactAttribute + { + } +} diff --git a/tests/XUnit/MSBuildFactDiscoverer.cs b/tests/XUnit/MSBuildFactDiscoverer.cs new file mode 100644 index 0000000000..77103021ac --- /dev/null +++ b/tests/XUnit/MSBuildFactDiscoverer.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +#nullable enable + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + + public sealed class MSBuildFactDiscoverer : IXunitTestCaseDiscoverer + { + private readonly FactDiscoverer _factDiscoverer; + + public MSBuildFactDiscoverer(IMessageSink diagnosticMessageSink) + { + _factDiscoverer = new FactDiscoverer(diagnosticMessageSink); + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + return _factDiscoverer + .Discover(discoveryOptions, testMethod, factAttribute) + .Select(testCase => new MSBuildTestCase(testCase)); + } + } +} diff --git a/tests/XUnit/MSBuildTestCase.cs b/tests/XUnit/MSBuildTestCase.cs new file mode 100644 index 0000000000..5e9e20b41f --- /dev/null +++ b/tests/XUnit/MSBuildTestCase.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Tools.Workspaces; +using Xunit; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + [DebuggerDisplay(@"\{ class = {TestMethod.TestClass.Class.Name}, method = {TestMethod.Method.Name}, display = {DisplayName}, skip = {SkipReason} \}")] + public sealed class MSBuildTestCase : LongLivedMarshalByRefObject, IXunitTestCase + { + private IXunitTestCase _testCase; + + public string DisplayName => _testCase.DisplayName; + public IMethodInfo Method => _testCase.Method; + public string SkipReason => _testCase.SkipReason; + public ITestMethod TestMethod => _testCase.TestMethod; + public object[] TestMethodArguments => _testCase.TestMethodArguments; + public Dictionary> Traits => _testCase.Traits; + public string UniqueID => _testCase.UniqueID; + + public ISourceInformation SourceInformation + { + get => _testCase.SourceInformation; + set => _testCase.SourceInformation = value; + } + + public Exception InitializationException => _testCase.InitializationException; + + public int Timeout => _testCase.Timeout; + + public MSBuildTestCase(IXunitTestCase testCase) + { + _testCase = testCase ?? throw new ArgumentNullException(nameof(testCase)); + } + + [Obsolete("Called by the deserializer", error: true)] + public MSBuildTestCase() { } + + public async Task RunAsync( + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + object[] constructorArguments, + ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) + { + await MSBuildWorkspaceLoader.Guard.WaitAsync(); + try + { + var runner = new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, TestMethodArguments, messageBus, aggregator, cancellationTokenSource); + return await runner.RunAsync(); + } + finally + { + MSBuildWorkspaceLoader.Guard.Release(); + } + } + + public void Deserialize(IXunitSerializationInfo info) + { + _testCase = info.GetValue("InnerTestCase"); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue("InnerTestCase", _testCase); + } + } +} diff --git a/tests/XUnit/MSBuildTheoryAttribute.cs b/tests/XUnit/MSBuildTheoryAttribute.cs new file mode 100644 index 0000000000..f239b84778 --- /dev/null +++ b/tests/XUnit/MSBuildTheoryAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System; +using Xunit; +using Xunit.Sdk; + +#nullable enable + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [XunitTestCaseDiscoverer("Microsoft.CodeAnalysis.Tools.Tests.XUnit.MSBuildTheoryDiscoverer", "dotnet-format.UnitTests")] + public sealed class MSBuildTheoryAttribute : TheoryAttribute + { + } +} diff --git a/tests/XUnit/MSBuildTheoryDiscoverer.cs b/tests/XUnit/MSBuildTheoryDiscoverer.cs new file mode 100644 index 0000000000..6c793bfa09 --- /dev/null +++ b/tests/XUnit/MSBuildTheoryDiscoverer.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +#nullable enable + +namespace Microsoft.CodeAnalysis.Tools.Tests.XUnit +{ + + public sealed class MSBuildTheoryDiscoverer : IXunitTestCaseDiscoverer + { + private readonly TheoryDiscoverer _theoryDiscoverer; + + public MSBuildTheoryDiscoverer(IMessageSink diagnosticMessageSink) + { + _theoryDiscoverer = new TheoryDiscoverer(diagnosticMessageSink); + } + + public IEnumerable Discover( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo factAttribute) + { + return _theoryDiscoverer + .Discover(discoveryOptions, testMethod, factAttribute) + .Select(testCase => new MSBuildTestCase(testCase)); + } + } +}