From 83e46311ed2e92bafa73580a35a53fdeaf5c1128 Mon Sep 17 00:00:00 2001 From: Jared Parsons Date: Tue, 20 Oct 2020 12:32:13 -0700 Subject: [PATCH] Use dotnet test (#48686) This change is migrating us away from the xUnit console runner and onto the `dotnet test` command instead. This has the following advantages: 1. xunit console runner is not a supported product hence we really need to move off it anyways. 2. Can take advantage of all the crash dump and blame analysis that they are doing in the dotnet test tool 3. Simplifies our story a bit because it means we are dealing with a single way to invoke tests across all the different configurations, OS we run. It's dotnet test all the time. This is true even when we are running .NET Framework tests. 4. One step closer to removing all of the restore logic from our test jobs --- eng/Versions.props | 3 + eng/build.ps1 | 37 +-- eng/targets/XUnit.targets | 4 + ...ortTestsWithAddImportDiagnosticProvider.vb | 15 +- .../Source/RunTests/AssemblyScheduler.cs | 232 ++++++++-------- src/Tools/Source/RunTests/ConsoleUtil.cs | 2 - src/Tools/Source/RunTests/FileUtil.cs | 2 - src/Tools/Source/RunTests/ITestExecutor.cs | 22 +- src/Tools/Source/RunTests/Logger.cs | 2 - src/Tools/Source/RunTests/Options.cs | 250 +++++++----------- src/Tools/Source/RunTests/ProcDumpUtil.cs | 2 - src/Tools/Source/RunTests/ProcessRunner.cs | 14 +- .../Source/RunTests/ProcessTestExecutor.cs | 94 ++++--- src/Tools/Source/RunTests/ProcessUtil.cs | 2 - src/Tools/Source/RunTests/Program.Json.cs | 30 --- src/Tools/Source/RunTests/Program.cs | 16 +- src/Tools/Source/RunTests/RunTests.csproj | 1 + src/Tools/Source/RunTests/TestRunner.cs | 6 +- .../ReferenceLocationExtensions.cs | 1 + 19 files changed, 313 insertions(+), 422 deletions(-) delete mode 100644 src/Tools/Source/RunTests/Program.Json.cs diff --git a/eng/Versions.props b/eng/Versions.props index 0cd918e6bd2ed..7b90bf027eff9 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -231,6 +231,7 @@ 0.10.0 $(xunitVersion) 1.3.2 + 2.1.26 $(xunitVersion) 2.4.1-pre.build.4059 1.0.51 @@ -266,5 +267,7 @@ rather explicitly override it. --> true + + true diff --git a/eng/build.ps1 b/eng/build.ps1 index b9bab9cc52f46..0a66d72d50814 100644 --- a/eng/build.ps1 +++ b/eng/build.ps1 @@ -352,12 +352,13 @@ function TestUsingOptimizedRunner() { ExitWithExitCode 1 } - $xunitDir = Join-Path (Get-PackageDir "xunit.runner.console") "tools\net472" - $args = "`"$xunitDir`"" - $args += " `"-out:$testResultsDir`"" - $args += " `"-logs:$LogDir`"" - $args += " `"-secondaryLogs:$secondaryLogDir`"" - $args += " -tfm:net472" + $dotnetExe = Join-Path $dotnet "dotnet.exe" + $args += " --dotnet `"$dotnetExe`"" + $args += " --out `"$testResultsDir`"" + $args += " --logs `"$LogDir`"" + $args += " --secondaryLogs `"$secondaryLogDir`"" + $args += " --tfm net472" + $args += " --html" if ($testDesktop -or $testIOperation) { if ($test32) { @@ -373,10 +374,10 @@ function TestUsingOptimizedRunner() { } $dlls += @(Get-ChildItem -Recurse -Include "*.IntegrationTests.dll" $binDir) - $args += " -testVsi" + $args += " --testVsi" } else { $dlls = Get-ChildItem -Recurse -Include "*.IntegrationTests.dll" $binDir - $args += " -trait:Feature=NetCore" + $args += " --trait:Feature=NetCore" } # Exclude out the multi-targetted netcore app projects @@ -396,26 +397,28 @@ function TestUsingOptimizedRunner() { $dlls = $dlls | ?{ -not (($_.FullName -match ".*\\$excludedConfiguration\\.*") -or ($_.FullName -match ".*/$excludedConfiguration/.*")) } if ($ci) { - $args += " -xml" if ($testVsi) { - $args += " -timeout:110" + $args += " --timeout 110" } else { - $args += " -timeout:90" + $args += " --timeout 90" } } - $procdumpPath = Ensure-ProcDump - $args += " -procdumppath:$procDumpPath" if ($procdump) { - $args += " -useprocdump"; + $procdumpFilePath = Ensure-ProcDump + $args += " --procdumppath $procDumpFilePath" + $args += " --useprocdump"; } if ($test64) { - $args += " -test64" + $args += " --platform x64" + } + else { + $args += " --platform x86" } if ($sequential) { - $args += " -sequential" + $args += " --sequential" } foreach ($dll in $dlls) { @@ -525,7 +528,7 @@ function Ensure-ProcDump() { Unzip $zipFilePath $outDir } - return $outDir + return $filePath } # Setup the CI machine for running our integration tests. diff --git a/eng/targets/XUnit.targets b/eng/targets/XUnit.targets index 08aff6f05b81a..d51023aaab798 100644 --- a/eng/targets/XUnit.targets +++ b/eng/targets/XUnit.targets @@ -6,6 +6,10 @@ $(PrepareForBuildDependsOn);AddDefaultTestAppConfig + + + + diff --git a/src/EditorFeatures/VisualBasicTest/Diagnostics/AddImport/AddImportTestsWithAddImportDiagnosticProvider.vb b/src/EditorFeatures/VisualBasicTest/Diagnostics/AddImport/AddImportTestsWithAddImportDiagnosticProvider.vb index e0959a9fe3b19..c329f0143f6a0 100644 --- a/src/EditorFeatures/VisualBasicTest/Diagnostics/AddImport/AddImportTestsWithAddImportDiagnosticProvider.vb +++ b/src/EditorFeatures/VisualBasicTest/Diagnostics/AddImport/AddImportTestsWithAddImportDiagnosticProvider.vb @@ -33,8 +33,7 @@ End Class", TestHost.InProcess) End Function - - + Public Async Function TestUnknownIdentifierGenericName() As Task Await TestAsync( "Class C @@ -54,8 +53,7 @@ End Class", TestHost.InProcess) End Function - - + Public Async Function TestUnknownIdentifierAddNamespaceImport() As Task Await TestAsync( "Class Class1 @@ -69,8 +67,7 @@ End Class", TestHost.InProcess) End Function - - + Public Async Function TestUnknownAttributeInModule() As Task Await TestAsync( "Module Goo @@ -108,8 +105,7 @@ Class MultiDictionary(Of K, V) End Class") End Function - - + Public Async Function TestImportIncompleteSub() As Task Await TestAsync( @@ -140,8 +136,7 @@ End Namespace", TestHost.InProcess) End Function - - + Public Async Function TestImportIncompleteSub2() As Task Await TestAsync( "Imports System.Linq diff --git a/src/Tools/Source/RunTests/AssemblyScheduler.cs b/src/Tools/Source/RunTests/AssemblyScheduler.cs index 46e8f35fe47ca..915e20bd979e6 100644 --- a/src/Tools/Source/RunTests/AssemblyScheduler.cs +++ b/src/Tools/Source/RunTests/AssemblyScheduler.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -14,38 +14,70 @@ namespace RunTests { - internal struct AssemblyInfo + internal readonly struct AssemblyInfo + { + internal PartitionInfo PartitionInfo { get; } + internal string TargetFramework { get; } + internal string Platform { get; } + + internal string AssemblyPath => PartitionInfo.AssemblyPath; + internal string AssemblyName => Path.GetFileName(PartitionInfo.AssemblyPath); + internal string DisplayName => PartitionInfo.DisplayName; + + internal AssemblyInfo(PartitionInfo partitionInfo, string targetFramework, string platform) + { + PartitionInfo = partitionInfo; + TargetFramework = targetFramework; + Platform = platform; + } + } + + internal readonly struct PartitionInfo { - internal readonly string AssemblyPath; - internal readonly string DisplayName; - internal readonly string ResultsFileName; - internal readonly string ExtraArguments; + internal int? AssemblyPartitionId { get; } + internal string AssemblyPath { get; } + internal string DisplayName { get; } + + /// + /// Specific set of types to test in the assembly. Will be empty when testing the entire assembly + /// + internal readonly ImmutableArray TypeInfoList; - internal AssemblyInfo( + internal PartitionInfo( + int assemblyPartitionId, string assemblyPath, string displayName, - string resultsFileName, - string extraArguments) + ImmutableArray typeInfoList) { + AssemblyPartitionId = assemblyPartitionId; AssemblyPath = assemblyPath; DisplayName = displayName; - ResultsFileName = resultsFileName; - ExtraArguments = extraArguments; + TypeInfoList = typeInfoList; } - internal AssemblyInfo(string assemblyPath, string targetFrameworkMoniker, string architecture, bool includeHtml) + internal PartitionInfo(string assemblyPath) { + AssemblyPartitionId = null; AssemblyPath = assemblyPath; DisplayName = Path.GetFileName(assemblyPath); - - var suffix = includeHtml ? "html" : "xml"; - ResultsFileName = $"{DisplayName}_{targetFrameworkMoniker}_{architecture}.{suffix}"; - ExtraArguments = string.Empty; + TypeInfoList = ImmutableArray.Empty; } public override string ToString() => DisplayName; } + public readonly struct TypeInfo + { + internal readonly string FullName; + internal readonly int MethodCount; + + internal TypeInfo(string fullName, int methodCount) + { + FullName = fullName; + MethodCount = methodCount; + } + } + internal sealed class AssemblyScheduler { /// @@ -58,128 +90,77 @@ internal sealed class AssemblyScheduler /// private const string EventListenerGuardFullName = "Microsoft.CodeAnalysis.UnitTests.EventListenerGuard"; - private struct TypeInfo - { - internal readonly string FullName; - internal readonly int MethodCount; - - internal TypeInfo(string fullName, int methodCount) - { - FullName = fullName; - MethodCount = methodCount; - } - } - - private struct Partition - { - internal readonly string AssemblyPath; - internal readonly int Id; - internal List TypeInfoList; - - internal Partition(string assemblyPath, int id, List typeInfoList) - { - AssemblyPath = assemblyPath; - Id = id; - TypeInfoList = typeInfoList; - } - } - - private sealed class AssemblyInfoBuilder + private static class AssemblyInfoBuilder { - private readonly List _partitionList = new List(); - private readonly List _assemblyInfoList = new List(); - private readonly StringBuilder _builder = new StringBuilder(); - private readonly string _assemblyPath; - private readonly int _methodLimit; - private readonly bool _includeHtml; - private readonly bool _hasEventListenerGuard; - private int _currentId; - private List _currentTypeInfoList = new List(); - - private AssemblyInfoBuilder(string assemblyPath, int methodLimit, bool includeHtml, bool hasEventListenerGuard) - { - _assemblyPath = assemblyPath; - _includeHtml = includeHtml; - _methodLimit = methodLimit; - _hasEventListenerGuard = hasEventListenerGuard; - } - - internal static void Build(string assemblyPath, int methodLimit, bool includeHtml, List typeInfoList, out List partitionList, out List assemblyInfoList) + internal static void Build(string assemblyPath, int methodLimit, List typeInfoList, out ImmutableArray partitionInfoList) { + var list = new List(); var hasEventListenerGuard = typeInfoList.Any(x => x.FullName == EventListenerGuardFullName); - var builder = new AssemblyInfoBuilder(assemblyPath, methodLimit, includeHtml, hasEventListenerGuard); - builder.Build(typeInfoList); - partitionList = builder._partitionList; - assemblyInfoList = builder._assemblyInfoList; - } + var currentTypeInfoList = new List(); + var currentClassNameLengthSum = -1; + var currentId = 0; - private void Build(List typeInfoList) - { BeginPartition(); foreach (var typeInfo in typeInfoList) { - _currentTypeInfoList.Add(typeInfo); - _builder.Append($@"-class ""{typeInfo.FullName}"" "); + currentTypeInfoList.Add(typeInfo); + currentClassNameLengthSum += typeInfo.FullName.Length; CheckForPartitionLimit(done: false); } CheckForPartitionLimit(done: true); - } - private void BeginPartition() - { - _currentId++; - _currentTypeInfoList = new List(); - _builder.Length = 0; + partitionInfoList = ImmutableArray.CreateRange(list); - // Ensure the EventListenerGuard is in every partition. - if (_hasEventListenerGuard) + void BeginPartition() { - _builder.Append($@"-class ""{EventListenerGuardFullName}"" "); + currentId++; + currentTypeInfoList.Clear(); + currentClassNameLengthSum = 0; + + // Ensure the EventListenerGuard is in every partition. + if (hasEventListenerGuard) + { + currentClassNameLengthSum += EventListenerGuardFullName.Length; + } } - } - private void CheckForPartitionLimit(bool done) - { - if (done) + void CheckForPartitionLimit(bool done) { - // The builder is done looking at types. If there are any TypeInfo that have not - // been added to a partition then do it now. - if (_currentTypeInfoList.Count > 0) + if (done) { - FinishPartition(); + // The builder is done looking at types. If there are any TypeInfo that have not + // been added to a partition then do it now. + if (currentTypeInfoList.Count > 0) + { + FinishPartition(); + } + + return; } - return; - } + // One item we have to consider here is the maximum command line length in + // Windows which is 32767 characters (XP is smaller but don't care). Once + // we get close then create a partition and move on. + if (currentTypeInfoList.Sum(x => x.MethodCount) >= methodLimit || + currentClassNameLengthSum > 25000) + { + FinishPartition(); + BeginPartition(); + } - // One item we have to consider here is the maximum command line length in - // Windows which is 32767 characters (XP is smaller but don't care). Once - // we get close then create a partition and move on. - if (_currentTypeInfoList.Sum(x => x.MethodCount) >= _methodLimit || - _builder.Length > 25000) - { - FinishPartition(); - BeginPartition(); + void FinishPartition() + { + var partitionInfo = new PartitionInfo( + currentId, + assemblyPath, + $"{Path.GetFileName(assemblyPath)}.{currentId}", + ImmutableArray.CreateRange(currentTypeInfoList)); + list.Add(partitionInfo); + } } } - - private void FinishPartition() - { - var assemblyName = Path.GetFileName(_assemblyPath); - var displayName = $"{assemblyName}.{_currentId}"; - var suffix = _includeHtml ? "html" : "xml"; - var resultsFileName = $"{assemblyName}.{_currentId}.{suffix}"; - var assemblyInfo = new AssemblyInfo( - _assemblyPath, - displayName, - resultsFileName, - _builder.ToString()); - - _partitionList.Add(new Partition(_assemblyPath, _currentId, _currentTypeInfoList)); - _assemblyInfoList.Add(assemblyInfo); - } } /// @@ -196,24 +177,22 @@ internal AssemblyScheduler(Options options, int methodLimit = DefaultMethodLimit _methodLimit = methodLimit; } - public IEnumerable Schedule(string assemblyPath, bool force = false) + public ImmutableArray Schedule(string assemblyPath, bool force = false) { if (_options.Sequential) { - return new[] { CreateAssemblyInfo(assemblyPath) }; + return ImmutableArray.Create(new PartitionInfo(assemblyPath)); } var typeInfoList = GetTypeInfoList(assemblyPath); - var assemblyInfoList = new List(); - var partitionList = new List(); - AssemblyInfoBuilder.Build(assemblyPath, _methodLimit, _options.IncludeHtml, typeInfoList, out partitionList, out assemblyInfoList); + AssemblyInfoBuilder.Build(assemblyPath, _methodLimit, typeInfoList, out var partitionList); // If the scheduling didn't actually produce multiple partition then send back an unpartitioned // representation. - if (assemblyInfoList.Count == 1 && !force) + if (partitionList.Length == 1 && !force) { Logger.Log($"Assembly schedule produced a single partition {assemblyPath}"); - return new[] { CreateAssemblyInfo(assemblyPath) }; + return ImmutableArray.Create(new PartitionInfo(assemblyPath)); } Logger.Log($"Assembly Schedule: {Path.GetFileName(assemblyPath)}"); @@ -221,19 +200,14 @@ public IEnumerable Schedule(string assemblyPath, bool force = fals { var methodCount = partition.TypeInfoList.Sum(x => x.MethodCount); var delta = methodCount - _methodLimit; - Logger.Log($" Partition: {partition.Id} method count {methodCount} delta {delta}"); + Logger.Log($" Partition: {partition.AssemblyPartitionId} method count {methodCount} delta {delta}"); foreach (var typeInfo in partition.TypeInfoList) { Logger.Log($" {typeInfo.FullName} {typeInfo.MethodCount}"); } } - return assemblyInfoList; - } - - public AssemblyInfo CreateAssemblyInfo(string assemblyPath) - { - return new AssemblyInfo(assemblyPath, _options.TargetFrameworkMoniker, _options.Test64 ? "x64" : "x86", _options.IncludeHtml); + return partitionList; } private static List GetTypeInfoList(string assemblyPath) diff --git a/src/Tools/Source/RunTests/ConsoleUtil.cs b/src/Tools/Source/RunTests/ConsoleUtil.cs index 61cc7a0056440..3f12672641cf8 100644 --- a/src/Tools/Source/RunTests/ConsoleUtil.cs +++ b/src/Tools/Source/RunTests/ConsoleUtil.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; namespace RunTests diff --git a/src/Tools/Source/RunTests/FileUtil.cs b/src/Tools/Source/RunTests/FileUtil.cs index 56968253bc30e..6325eacec2586 100644 --- a/src/Tools/Source/RunTests/FileUtil.cs +++ b/src/Tools/Source/RunTests/FileUtil.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System.IO; namespace RunTests diff --git a/src/Tools/Source/RunTests/ITestExecutor.cs b/src/Tools/Source/RunTests/ITestExecutor.cs index 689aaa8e66ba0..9ffe3fd80161e 100644 --- a/src/Tools/Source/RunTests/ITestExecutor.cs +++ b/src/Tools/Source/RunTests/ITestExecutor.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Immutable; using System.IO; @@ -12,24 +10,22 @@ namespace RunTests { internal readonly struct TestExecutionOptions { - internal string XunitPath { get; } + internal string DotnetFilePath { get; } internal ProcDumpInfo? ProcDumpInfo { get; } - internal string OutputDirectory { get; } - internal string Trait { get; } - internal string NoTrait { get; } + internal string TestResultsDirectory { get; } + internal string? Trait { get; } + internal string? NoTrait { get; } internal bool IncludeHtml { get; } - internal bool Test64 { get; } internal bool TestVsi { get; } - internal TestExecutionOptions(string xunitPath, ProcDumpInfo? procDumpInfo, string outputDirectory, string trait, string noTrait, bool includeHtml, bool test64, bool testVsi) + internal TestExecutionOptions(string dotnetFilePath, ProcDumpInfo? procDumpInfo, string testResultsDirectory, string? trait, string? noTrait, bool includeHtml, bool testVsi) { - XunitPath = xunitPath; + DotnetFilePath = dotnetFilePath; ProcDumpInfo = procDumpInfo; - OutputDirectory = outputDirectory; + TestResultsDirectory = testResultsDirectory; Trait = trait; NoTrait = noTrait; IncludeHtml = includeHtml; - Test64 = test64; TestVsi = testVsi; } } @@ -69,7 +65,7 @@ internal readonly struct TestResult internal TestResultInfo TestResultInfo { get; } internal AssemblyInfo AssemblyInfo { get; } internal string CommandLine { get; } - internal string Diagnostics { get; } + internal string? Diagnostics { get; } /// /// Collection of processes the runner explicitly ran to get the result. @@ -86,7 +82,7 @@ internal readonly struct TestResult internal string ErrorOutput => TestResultInfo.ErrorOutput; internal string ResultsFilePath => TestResultInfo.ResultsFilePath; - internal TestResult(AssemblyInfo assemblyInfo, TestResultInfo testResultInfo, string commandLine, ImmutableArray processResults = default, string diagnostics = null) + internal TestResult(AssemblyInfo assemblyInfo, TestResultInfo testResultInfo, string commandLine, ImmutableArray processResults = default, string? diagnostics = null) { AssemblyInfo = assemblyInfo; TestResultInfo = testResultInfo; diff --git a/src/Tools/Source/RunTests/Logger.cs b/src/Tools/Source/RunTests/Logger.cs index 4b79fd70ce2f1..846dfb01d444b 100644 --- a/src/Tools/Source/RunTests/Logger.cs +++ b/src/Tools/Source/RunTests/Logger.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.IO; diff --git a/src/Tools/Source/RunTests/Options.cs b/src/Tools/Source/RunTests/Options.cs index 23c8836b89db2..a5e705ef50058 100644 --- a/src/Tools/Source/RunTests/Options.cs +++ b/src/Tools/Source/RunTests/Options.cs @@ -2,13 +2,13 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; +using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using Mono.Options; namespace RunTests { @@ -36,7 +36,7 @@ internal class Options /// Target framework used to run the tests, e.g. "net472". /// This is currently only used to name the test result files. /// - public string TargetFrameworkMoniker { get; set; } + public string TargetFramework { get; set; } /// /// Use the open integration test runner. @@ -51,17 +51,17 @@ internal class Options /// /// Trait string to pass to xunit. /// - public string Trait { get; set; } + public string? Trait { get; set; } /// /// The no-trait string to pass to xunit. /// - public string NoTrait { get; set; } + public string? NoTrait { get; set; } /// /// Set of assemblies to test. /// - public List Assemblies { get; set; } + public List Assemblies { get; set; } = new List(); /// /// Time after which the runner should kill the xunit process and exit with a failure. @@ -73,196 +73,140 @@ internal class Options /// public bool UseProcDump { get; set; } + /// + /// The path to procdump.exe + /// + public string? ProcDumpFilePath { get; set; } + /// /// Disable partitioning and parallelization across test assemblies. /// public bool Sequential { get; set; } /// - /// The directory which contains procdump.exe. + /// Path to the dotnet executable we should use for running dotnet test /// - public string ProcDumpDirectory { get; set; } - - public string XunitPath { get; set; } + public string DotnetFilePath { get; set; } /// /// Directory to hold all of the xml files created as test results. /// - public string TestResultXmlOutputDirectory { get; set; } + public string TestResultsDirectory { get; set; } /// /// Directory to hold dump files and other log files created while running tests. /// - public string LogFilesOutputDirectory { get; set; } + public string LogFilesDirectory { get; set; } /// /// Directory to hold secondary dump files created while running tests. /// - public string LogFilesSecondaryOutputDirectory { get; set; } - - internal static Options Parse(string[] args) - { - if (args == null || args.Any(a => a == null) || args.Length < 2) - { - return null; - } + public string LogFilesSecondaryDirectory { get; set; } - var comparer = StringComparer.OrdinalIgnoreCase; - bool isOption(string argument, string optionName, out string value) - { - Debug.Assert(!string.IsNullOrEmpty(optionName) && optionName[0] == '-'); - if (argument.StartsWith(optionName + ":", StringComparison.OrdinalIgnoreCase)) - { - value = argument.Substring(optionName.Length + 1); - return !string.IsNullOrEmpty(value); - } + public string Platform { get; set; } - value = null; - return false; - } + public Options( + string dotnetFilePath, + string testResultsDirectory, + string logFilesDirectory, + string logFilesSecondaryDirectory, + string targetFramework, + string platform) + { + DotnetFilePath = dotnetFilePath; + TestResultsDirectory = testResultsDirectory; + LogFilesDirectory = logFilesDirectory; + LogFilesSecondaryDirectory = logFilesSecondaryDirectory; + TargetFramework = targetFramework; + Platform = platform; + } - var opt = new Options { XunitPath = args[0], IncludeHtml = true, TestResultXmlOutputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestResults") }; - var index = 1; - var allGood = true; - while (index < args.Length) + internal static Options? Parse(string[] args) + { + string? dotnetFilePath = null; + var platform = "x64"; + var testVsi = false; + var includeHtml = false; + var targetFramework = "net472"; + var sequential = false; + string? traits = null; + string? noTraits = null; + int? timeout = null; + string resultFileDirectory = Path.Combine(Directory.GetCurrentDirectory(), "TestResults"); + string? logFileDirectory = null; + string? logFileSecondaryDirectory = null; + var display = Display.None; + var useProcDump = false; + string? procDumpFilePath = null; + var optionSet = new OptionSet() { - var current = args[index]; - if (comparer.Equals(current, "-test64")) - { - opt.Test64 = true; - index++; - } - else if (comparer.Equals(current, "-testVsi")) - { - opt.TestVsi = true; - index++; - } - else if (comparer.Equals(current, "-xml")) - { - opt.IncludeHtml = false; - index++; - } - else if (isOption(current, "-tfm", out string targetFrameworkMoniker)) - { - opt.TargetFrameworkMoniker = targetFrameworkMoniker; - index++; - } - else if (isOption(current, "-out", out string value)) - { - opt.TestResultXmlOutputDirectory = value; - index++; - } - else if (isOption(current, "-logs", out string logsPath)) - { - opt.LogFilesOutputDirectory = logsPath; - index++; - } - else if (isOption(current, "-secondaryLogs", out string secondaryLogsPath)) - { - opt.LogFilesSecondaryOutputDirectory = secondaryLogsPath; - index++; - } - else if (isOption(current, "-display", out value)) - { - if (Enum.TryParse(value, ignoreCase: true, result: out Display display)) - { - opt.Display = display; - } - else - { - Console.WriteLine($"{value} is not a valid option for display"); - allGood = false; - } - - index++; - } - else if (isOption(current, "-trait", out value)) - { - opt.Trait = value; - index++; - } - else if (isOption(current, "-notrait", out value)) - { - opt.NoTrait = value; - index++; - } - else if (isOption(current, "-timeout", out value)) - { - if (int.TryParse(value, out var minutes)) - { - opt.Timeout = TimeSpan.FromMinutes(minutes); - } - else - { - Console.WriteLine($"{value} is not a valid minute value for timeout"); - allGood = false; - } - - index++; - } - else if (isOption(current, "-procdumpPath", out value)) - { - opt.ProcDumpDirectory = value; - index++; - } - else if (comparer.Equals(current, "-useprocdump")) - { - opt.UseProcDump = false; - index++; - } - else if (comparer.Equals(current, "-sequential")) - { - opt.Sequential = true; - index++; - } - else - { - break; - } - } - + { "dotnet=", "Path to dotnet", (string s) => dotnetFilePath = s }, + { "platform=", "Platform to test: x86 or x64", (string s) => platform = s }, + { "tfm=", "Target framework to test", (string s) => targetFramework = s }, + { "testVsi", "Test Visual Studio", o => testVsi = o is object }, + { "html", "Include HTML file output", o => includeHtml = o is object }, + { "sequential", "Run tests sequentially", o => sequential = o is object }, + { "traits=", "xUnit traits to include (semicolon delimited)", (string s) => traits = s }, + { "noTraits=", "xUnit traits to exclude (semicolon delimited)", (string s) => noTraits = s }, + { "timeout=", "Minute timeout to limit the tests to", (int i) => timeout = i }, + { "out=", "Test result file directory", (string s) => resultFileDirectory = s }, + { "logs=", "Log file directory", (string s) => logFileDirectory = s }, + { "secondaryLogs=", "Log secondary file directory", (string s) => logFileSecondaryDirectory = s }, + { "display=", "Display", (Display d) => display = d }, + { "procdumpPath=", "Path to procdump", (string s) => procDumpFilePath = s }, + { "useProcdump", "Whether or not to use procdump", o => useProcDump = o is object }, + }; + + List assemblyList; try { - opt.XunitPath = opt.Test64 - ? Path.Combine(opt.XunitPath, "xunit.console.exe") - : Path.Combine(opt.XunitPath, "xunit.console.x86.exe"); + assemblyList = optionSet.Parse(args); } - catch (ArgumentException ex) + catch (OptionException e) { - Console.WriteLine($"{opt.XunitPath} is not a valid path: {ex.Message}"); + Console.WriteLine($"Error parsing command line arguments: {e.Message}"); + optionSet.WriteOptionDescriptions(Console.Out); return null; } - if (!File.Exists(opt.XunitPath)) + if (dotnetFilePath is null || !File.Exists(dotnetFilePath)) { - Console.WriteLine($"The file '{opt.XunitPath}' does not exist."); + Console.WriteLine($"Did not find 'dotnet' at {dotnetFilePath}"); return null; } - if (opt.UseProcDump && string.IsNullOrEmpty(opt.ProcDumpDirectory)) + if (useProcDump && string.IsNullOrEmpty(procDumpFilePath)) { Console.WriteLine($"The option 'useprocdump' was specified but 'procdumppath' was not provided"); return null; } - // If we weren't passed both -logs and -out but just -out, use the same value for -logs too. - if (opt.LogFilesOutputDirectory == null) + if (logFileDirectory is null) { - opt.LogFilesOutputDirectory = opt.TestResultXmlOutputDirectory; + logFileDirectory = resultFileDirectory; } - // If we weren't passed both -secondaryLogs and -logs but just -logs (or -out), use the same value for -secondaryLogs too. - opt.LogFilesSecondaryOutputDirectory ??= opt.LogFilesOutputDirectory; - - opt.Assemblies = args.Skip(index).ToList(); - return allGood ? opt : null; - } + logFileSecondaryDirectory ??= logFileDirectory; - public static void PrintUsage() - { - Console.WriteLine("runtests [xunit-console-runner] [-test64] [-xml] [-trait:name1=value1;...] [-notrait:name1=value1;...] [assembly1] [assembly2] [...]"); - Console.WriteLine("Example:"); - Console.WriteLine(@"runtests c:\path-that-contains-xunit.console.exe\ -trait:Feature=Classification Assembly1.dll Assembly2.dll"); + return new Options( + dotnetFilePath: dotnetFilePath, + testResultsDirectory: resultFileDirectory, + logFilesDirectory: logFileDirectory, + logFilesSecondaryDirectory: logFileSecondaryDirectory, + targetFramework: targetFramework, + platform: platform) + { + Assemblies = assemblyList, + TestVsi = testVsi, + Display = display, + ProcDumpFilePath = procDumpFilePath, + UseProcDump = useProcDump, + Sequential = sequential, + IncludeHtml = includeHtml, + Trait = traits, + NoTrait = noTraits, + Timeout = timeout is { } t ? TimeSpan.FromMinutes(t) : null, + }; } } } diff --git a/src/Tools/Source/RunTests/ProcDumpUtil.cs b/src/Tools/Source/RunTests/ProcDumpUtil.cs index c2550a47a2df3..389cddf00b06b 100644 --- a/src/Tools/Source/RunTests/ProcDumpUtil.cs +++ b/src/Tools/Source/RunTests/ProcDumpUtil.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/src/Tools/Source/RunTests/ProcessRunner.cs b/src/Tools/Source/RunTests/ProcessRunner.cs index b3c07b2f39259..bdb555150ee4f 100644 --- a/src/Tools/Source/RunTests/ProcessRunner.cs +++ b/src/Tools/Source/RunTests/ProcessRunner.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -60,11 +58,11 @@ public static ProcessInfo CreateProcess( string executable, string arguments, bool lowPriority = false, - string workingDirectory = null, + string? workingDirectory = null, bool captureOutput = false, bool displayWindow = true, - Dictionary environmentVariables = null, - Action onProcessStartHandler = null, + Dictionary? environmentVariables = null, + Action? onProcessStartHandler = null, CancellationToken cancellationToken = default) => CreateProcess( CreateProcessStartInfo(executable, arguments, workingDirectory, captureOutput, displayWindow, environmentVariables), @@ -75,7 +73,7 @@ public static ProcessInfo CreateProcess( public static ProcessInfo CreateProcess( ProcessStartInfo processStartInfo, bool lowPriority = false, - Action onProcessStartHandler = null, + Action? onProcessStartHandler = null, CancellationToken cancellationToken = default) { var errorLines = new List(); @@ -162,10 +160,10 @@ public static ProcessInfo CreateProcess( public static ProcessStartInfo CreateProcessStartInfo( string executable, string arguments, - string workingDirectory = null, + string? workingDirectory = null, bool captureOutput = false, bool displayWindow = true, - Dictionary environmentVariables = null) + Dictionary? environmentVariables = null) { var processStartInfo = new ProcessStartInfo(executable, arguments); diff --git a/src/Tools/Source/RunTests/ProcessTestExecutor.cs b/src/Tools/Source/RunTests/ProcessTestExecutor.cs index fa8d55eab7dbb..22eb0d446b6a8 100644 --- a/src/Tools/Source/RunTests/ProcessTestExecutor.cs +++ b/src/Tools/Source/RunTests/ProcessTestExecutor.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -25,52 +26,68 @@ internal ProcessTestExecutor(TestExecutionOptions options) Options = options; } - public string GetCommandLine(AssemblyInfo assemblyInfo) - { - return $"{Options.XunitPath} {GetCommandLineArguments(assemblyInfo)}"; - } - public string GetCommandLineArguments(AssemblyInfo assemblyInfo) { var assemblyName = Path.GetFileName(assemblyInfo.AssemblyPath); - var resultsFilePath = GetResultsFilePath(assemblyInfo); - var xmlResultsFilePath = Path.ChangeExtension(resultsFilePath, ".xml"); - var htmlResultsFilePath = Path.ChangeExtension(resultsFilePath, ".html"); var builder = new StringBuilder(); - builder.AppendFormat(@"""{0}""", assemblyInfo.AssemblyPath); - builder.AppendFormat(@" {0}", assemblyInfo.ExtraArguments); - builder.AppendFormat($@" -xml ""{xmlResultsFilePath}"""); + builder.Append($@"test"); + builder.Append($@" ""{assemblyInfo.AssemblyPath}"""); + var typeInfoList = assemblyInfo.PartitionInfo.TypeInfoList; + if (typeInfoList.Length > 0 || !string.IsNullOrWhiteSpace(Options.Trait) || !string.IsNullOrWhiteSpace(Options.NoTrait)) + { + builder.Append(" --filter "); + var any = false; + foreach (var typeInfo in typeInfoList) + { + MaybeAddSeparator(); + builder.Append(typeInfo.FullName); + } - if (Options.IncludeHtml) - builder.AppendFormat($@" -html ""{htmlResultsFilePath}"""); + if (Options.Trait is object) + { + foreach (var trait in Options.Trait.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + MaybeAddSeparator(); + builder.Append($"Trait={trait}"); + } + } - builder.Append(" -noshadow -verbose"); + if (Options.NoTrait is object) + { + foreach (var trait in Options.NoTrait.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) + { + MaybeAddSeparator('&'); + builder.Append($"Trait!~{trait}"); + } + } - if (!string.IsNullOrWhiteSpace(Options.Trait)) - { - var traits = Options.Trait.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var trait in traits) + void MaybeAddSeparator(char separator = '|') { - builder.AppendFormat(" -trait {0}", trait); + if (any) + { + builder.Append(separator); + } + + any = true; } } - if (!string.IsNullOrWhiteSpace(Options.NoTrait)) + builder.Append($@" --framework {assemblyInfo.TargetFramework}"); + builder.Append($@" --logger ""xunit;LogFilePath={GetResultsFilePath(assemblyInfo, "xml")}"""); + + if (Options.IncludeHtml) { - var traits = Options.NoTrait.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (var trait in traits) - { - builder.AppendFormat(" -notrait {0}", trait); - } + builder.AppendFormat($@" --logger ""html;LogFileName={GetResultsFilePath(assemblyInfo, "html")}"""); } return builder.ToString(); } - private string GetResultsFilePath(AssemblyInfo assemblyInfo) + private string GetResultsFilePath(AssemblyInfo assemblyInfo, string suffix = "xml") { - return Path.Combine(Options.OutputDirectory, assemblyInfo.ResultsFileName); + var fileName = $"{assemblyInfo.DisplayName}_{assemblyInfo.TargetFramework}_{assemblyInfo.Platform}.{suffix}"; + return Path.Combine(Options.TestResultsDirectory, fileName); } public async Task RunTestAsync(AssemblyInfo assemblyInfo, CancellationToken cancellationToken) @@ -133,16 +150,16 @@ private async Task RunTestAsyncInternal(AssemblyInfo assemblyInfo, b File.Create(resultsFilePath).Close(); var start = DateTime.UtcNow; - var xunitProcessInfo = ProcessRunner.CreateProcess( + var dotnetProcessInfo = ProcessRunner.CreateProcess( ProcessRunner.CreateProcessStartInfo( - Options.XunitPath, + Options.DotnetFilePath, commandLineArguments, displayWindow: false, captureOutput: true, environmentVariables: environmentVariables), lowPriority: false, cancellationToken: cancellationToken); - Logger.Log($"Create xunit process with id {xunitProcessInfo.Id} for test {assemblyInfo.DisplayName}"); + Logger.Log($"Create xunit process with id {dotnetProcessInfo.Id} for test {assemblyInfo.DisplayName}"); // Now that xunit is running we should kick off a procDump process if it was specified if (Options.ProcDumpInfo != null) @@ -150,23 +167,23 @@ private async Task RunTestAsyncInternal(AssemblyInfo assemblyInfo, b var procDumpInfo = Options.ProcDumpInfo.Value; var procDumpStartInfo = ProcessRunner.CreateProcessStartInfo( procDumpInfo.ProcDumpFilePath, - ProcDumpUtil.GetProcDumpCommandLine(xunitProcessInfo.Id, procDumpInfo.DumpDirectory), + ProcDumpUtil.GetProcDumpCommandLine(dotnetProcessInfo.Id, procDumpInfo.DumpDirectory), captureOutput: true, displayWindow: false); Directory.CreateDirectory(procDumpInfo.DumpDirectory); procDumpProcessInfo = ProcessRunner.CreateProcess(procDumpStartInfo, cancellationToken: cancellationToken); - Logger.Log($"Create procdump process with id {procDumpProcessInfo.Value.Id} for xunit {xunitProcessInfo.Id} for test {assemblyInfo.DisplayName}"); + Logger.Log($"Create procdump process with id {procDumpProcessInfo.Value.Id} for xunit {dotnetProcessInfo.Id} for test {assemblyInfo.DisplayName}"); } - var xunitProcessResult = await xunitProcessInfo.Result; + var xunitProcessResult = await dotnetProcessInfo.Result; var span = DateTime.UtcNow - start; - Logger.Log($"Exit xunit process with id {xunitProcessInfo.Id} for test {assemblyInfo.DisplayName} with code {xunitProcessResult.ExitCode}"); + Logger.Log($"Exit xunit process with id {dotnetProcessInfo.Id} for test {assemblyInfo.DisplayName} with code {xunitProcessResult.ExitCode}"); processResultList.Add(xunitProcessResult); if (procDumpProcessInfo != null) { var procDumpProcessResult = await procDumpProcessInfo.Value.Result; - Logger.Log($"Exit procdump process with id {procDumpProcessInfo.Value.Id} for {xunitProcessInfo.Id} for test {assemblyInfo.DisplayName} with code {procDumpProcessResult.ExitCode}"); + Logger.Log($"Exit procdump process with id {procDumpProcessInfo.Value.Id} for {dotnetProcessInfo.Id} for test {assemblyInfo.DisplayName} with code {procDumpProcessResult.ExitCode}"); processResultList.Add(procDumpProcessResult); } @@ -194,8 +211,7 @@ private async Task RunTestAsyncInternal(AssemblyInfo assemblyInfo, b } } - var commandLine = GetCommandLine(assemblyInfo); - Logger.Log($"Command line {assemblyInfo.DisplayName}: {commandLine}"); + Logger.Log($"Command line {assemblyInfo.DisplayName}: {Options.DotnetFilePath} {commandLineArguments}"); var standardOutput = string.Join(Environment.NewLine, xunitProcessResult.OutputLines) ?? ""; var errorOutput = string.Join(Environment.NewLine, xunitProcessResult.ErrorLines) ?? ""; var testResultInfo = new TestResultInfo( @@ -208,12 +224,12 @@ private async Task RunTestAsyncInternal(AssemblyInfo assemblyInfo, b return new TestResult( assemblyInfo, testResultInfo, - commandLine, + commandLineArguments, processResults: ImmutableArray.CreateRange(processResultList)); } catch (Exception ex) { - throw new Exception($"Unable to run {assemblyInfo.AssemblyPath} with {Options.XunitPath}. {ex}"); + throw new Exception($"Unable to run {assemblyInfo.AssemblyPath} with {Options.DotnetFilePath}. {ex}"); } } } diff --git a/src/Tools/Source/RunTests/ProcessUtil.cs b/src/Tools/Source/RunTests/ProcessUtil.cs index c6e70deada2db..2df47acf51016 100644 --- a/src/Tools/Source/RunTests/ProcessUtil.cs +++ b/src/Tools/Source/RunTests/ProcessUtil.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/src/Tools/Source/RunTests/Program.Json.cs b/src/Tools/Source/RunTests/Program.Json.cs deleted file mode 100644 index 4a293aced4817..0000000000000 --- a/src/Tools/Source/RunTests/Program.Json.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#nullable disable - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace RunTests -{ - internal partial class Program - { - internal sealed class TestRunData - { - public int ElapsedSeconds { get; set; } - public bool Succeeded { get; set; } - public bool IsJenkins { get; set; } - public bool Is32Bit { get; set; } - public int AssemblyCount { get; set; } - public int CacheCount { get; set; } - public int ChunkCount { get; set; } - public string JenkinsUrl { get; set; } - public bool HasErrors { get; set; } - } - } -} diff --git a/src/Tools/Source/RunTests/Program.cs b/src/Tools/Source/RunTests/Program.cs index ee58597e9d9fc..87f1a516a5ed8 100644 --- a/src/Tools/Source/RunTests/Program.cs +++ b/src/Tools/Source/RunTests/Program.cs @@ -40,7 +40,6 @@ internal static int Main(string[] args) var options = Options.Parse(args); if (options == null) { - Options.PrintUsage(); return ExitFailure; } @@ -107,7 +106,7 @@ private static async Task RunCore(Options options, CancellationToken cancel var start = DateTime.Now; var assemblyInfoList = GetAssemblyList(options); - ConsoleUtil.WriteLine($"Proc dump location: {options.ProcDumpDirectory}"); + ConsoleUtil.WriteLine($"Proc dump location: {options.ProcDumpFilePath}"); ConsoleUtil.WriteLine($"Running {options.Assemblies.Count} test assemblies in {assemblyInfoList.Count} partitions"); var result = await testRunner.RunAllAsync(assemblyInfoList, cancellationToken).ConfigureAwait(true); @@ -157,7 +156,7 @@ private static void LogProcessResultDetails(ImmutableArray proces private static void WriteLogFile(Options options) { - var logFilePath = Path.Combine(options.LogFilesOutputDirectory, "runtests.log"); + var logFilePath = Path.Combine(options.LogFilesDirectory, "runtests.log"); try { using (var writer = new StreamWriter(logFilePath, append: false)) @@ -245,9 +244,9 @@ async Task DumpProcess(Process targetProcess, string procDumpExeFilePath, string private static ProcDumpInfo? GetProcDumpInfo(Options options) { - if (!string.IsNullOrEmpty(options.ProcDumpDirectory)) + if (!string.IsNullOrEmpty(options.ProcDumpFilePath)) { - return new ProcDumpInfo(Path.Combine(options.ProcDumpDirectory, "procdump.exe"), options.LogFilesOutputDirectory, options.LogFilesSecondaryOutputDirectory); + return new ProcDumpInfo(options.ProcDumpFilePath, options.LogFilesDirectory, options.LogFilesSecondaryDirectory); } return null; @@ -290,7 +289,7 @@ private static List GetAssemblyList(Options options) foreach (var assemblyPath in options.Assemblies.OrderByDescending(x => new FileInfo(x).Length)) { - list.AddRange(scheduler.Schedule(assemblyPath)); + list.AddRange(scheduler.Schedule(assemblyPath).Select(x => new AssemblyInfo(x, options.TargetFramework, options.Platform))); } return list; @@ -327,13 +326,12 @@ private static void DisplayResults(Display display, ImmutableArray t private static ProcessTestExecutor CreateTestExecutor(Options options) { var testExecutionOptions = new TestExecutionOptions( - xunitPath: options.XunitPath, + dotnetFilePath: options.DotnetFilePath, procDumpInfo: options.UseProcDump ? GetProcDumpInfo(options) : null, - outputDirectory: options.TestResultXmlOutputDirectory, + testResultsDirectory: options.TestResultsDirectory, trait: options.Trait, noTrait: options.NoTrait, includeHtml: options.IncludeHtml, - test64: options.Test64, testVsi: options.TestVsi); return new ProcessTestExecutor(testExecutionOptions); } diff --git a/src/Tools/Source/RunTests/RunTests.csproj b/src/Tools/Source/RunTests/RunTests.csproj index a68bfd0360b40..11d81724169ad 100644 --- a/src/Tools/Source/RunTests/RunTests.csproj +++ b/src/Tools/Source/RunTests/RunTests.csproj @@ -18,5 +18,6 @@ + \ No newline at end of file diff --git a/src/Tools/Source/RunTests/TestRunner.cs b/src/Tools/Source/RunTests/TestRunner.cs index 8eccce8af37c1..40405e74bdcc8 100644 --- a/src/Tools/Source/RunTests/TestRunner.cs +++ b/src/Tools/Source/RunTests/TestRunner.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#nullable disable - using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -149,14 +147,14 @@ private void Print(List testResults) ConsoleUtil.WriteLine("Extra run diagnostics for logging, did not impact run results"); foreach (var testResult in testResults.Where(x => !string.IsNullOrEmpty(x.Diagnostics))) { - ConsoleUtil.WriteLine(testResult.Diagnostics); + ConsoleUtil.WriteLine(testResult.Diagnostics!); } } private void PrintFailedTestResult(TestResult testResult) { // Save out the error output for easy artifact inspecting - var outputLogPath = Path.Combine(_options.LogFilesOutputDirectory, $"xUnitFailure-{testResult.DisplayName}.log"); + var outputLogPath = Path.Combine(_options.LogFilesDirectory, $"xUnitFailure-{testResult.DisplayName}.log"); ConsoleUtil.WriteLine($"Errors {testResult.AssemblyName}"); ConsoleUtil.WriteLine(testResult.ErrorOutput); diff --git a/src/Workspaces/Core/Portable/FindSymbols/ReferenceLocationExtensions.cs b/src/Workspaces/Core/Portable/FindSymbols/ReferenceLocationExtensions.cs index f041327c1d497..be877c762a146 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/ReferenceLocationExtensions.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/ReferenceLocationExtensions.cs @@ -39,6 +39,7 @@ public static async Task>> FindReferencingSym AddSymbols(semanticModel, documentGroup, result); } + // Keep compilation alive so that GetSemanticModelAsync remains cheap GC.KeepAlive(compilation); } }