Skip to content

Commit f9d61b5

Browse files
committed
#317: Slow performance on discovery / running due to discovering TestReporters
1 parent 01ec27d commit f9d61b5

File tree

6 files changed

+104
-156
lines changed

6 files changed

+104
-156
lines changed

Versions.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<NerdbankGitVersioningVersion>3.6.133</NerdbankGitVersioningVersion>
99
<NSubstituteVersion>5.0.0</NSubstituteVersion>
1010
<TunnelVisionLabsReferenceAssemblyAnnotatorVersion>1.0.0-alpha.160</TunnelVisionLabsReferenceAssemblyAnnotatorVersion>
11-
<XunitVersion>2.5.0-pre.26</XunitVersion>
11+
<XunitVersion>2.5.0-pre.32</XunitVersion>
1212
</PropertyGroup>
1313

1414
</Project>

src/xunit.runner.visualstudio/Utility/AssemblyExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#if NETFRAMEWORK
2-
31
using System;
42
using System.IO;
53
using System.Reflection;
@@ -8,6 +6,7 @@ internal static class AssemblyExtensions
86
{
97
public static string? GetLocalCodeBase(this Assembly assembly)
108
{
9+
#if NETFRAMEWORK
1110
string? codeBase = assembly.CodeBase;
1211
if (codeBase == null)
1312
return null;
@@ -20,7 +19,9 @@ internal static class AssemblyExtensions
2019
return "/" + codeBase;
2120

2221
return codeBase.Replace('/', Path.DirectorySeparatorChar);
22+
#else
23+
return assembly.Location;
24+
#endif
2325
}
2426
}
2527

26-
#endif

src/xunit.runner.visualstudio/VsTestRunner.cs

Lines changed: 17 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Diagnostics;
55
using System.IO;
66
using System.Linq;
7-
using System.Reflection;
87
using System.Runtime.InteropServices;
98
using System.Threading;
109
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
@@ -608,19 +607,19 @@ void handler()
608607
}
609608

610609
public static IRunnerReporter GetRunnerReporter(
611-
LoggerHelper logger,
610+
LoggerHelper? logger,
612611
RunSettings runSettings,
613612
IReadOnlyList<string> assemblyFileNames)
614613
{
615614
var reporter = default(IRunnerReporter);
616-
var availableReporters = new Lazy<IReadOnlyList<IRunnerReporter>>(() => GetAvailableRunnerReporters(assemblyFileNames));
615+
var availableReporters = new Lazy<IReadOnlyList<IRunnerReporter>>(() => GetAvailableRunnerReporters(logger, assemblyFileNames));
617616

618617
try
619618
{
620619
if (!string.IsNullOrEmpty(runSettings.ReporterSwitch))
621620
{
622621
reporter = availableReporters.Value.FirstOrDefault(r => string.Equals(r.RunnerSwitch, runSettings.ReporterSwitch, StringComparison.OrdinalIgnoreCase));
623-
if (reporter is null)
622+
if (reporter is null && logger is not null)
624623
logger.LogWarning("Could not find requested reporter '{0}'", runSettings.ReporterSwitch);
625624
}
626625

@@ -632,119 +631,30 @@ public static IRunnerReporter GetRunnerReporter(
632631
return reporter ?? new DefaultRunnerReporterWithTypes();
633632
}
634633

635-
static IReadOnlyList<IRunnerReporter> GetAvailableRunnerReporters(IReadOnlyList<string> sources)
634+
public static IReadOnlyList<IRunnerReporter> GetAvailableRunnerReporters(
635+
LoggerHelper? logger,
636+
IReadOnlyList<string> sources)
636637
{
637-
#if NETCOREAPP
638-
// Combine all input libs and merge their contexts to find the potential reporters
639638
var result = new List<IRunnerReporter>();
640-
var dcjr = new DependencyContextJsonReader();
641-
var deps =
642-
sources
643-
.Select(Path.GetFullPath)
644-
.Select(s => s.Replace(".dll", ".deps.json"))
645-
.Where(File.Exists)
646-
.Select(f => new MemoryStream(Encoding.UTF8.GetBytes(File.ReadAllText(f))))
647-
.Select(dcjr.Read);
648-
var ctx = deps.Aggregate(DependencyContext.Default, (context, dependencyContext) => context.Merge(dependencyContext));
649-
dcjr.Dispose();
650-
651-
var depsAssms = ctx.GetRuntimeAssemblyNames(InternalRuntimeEnvironment.GetRuntimeIdentifier()).ToList();
652-
653-
// Make sure to also check assemblies within the directory of the sources
654-
var dllsInSources =
639+
640+
// We need to combine the source folders with our folder to find all potential runners
641+
var folders =
655642
sources
656-
.Select(Path.GetFullPath)
657-
.Select(Path.GetDirectoryName)
658-
.Distinct(StringComparer.OrdinalIgnoreCase)
643+
.Select(s => Path.GetDirectoryName(Path.GetFullPath(s)))
659644
.WhereNotNull()
660-
.SelectMany(p => Directory.GetFiles(p, "*.dll").Select(f => Path.Combine(p, f)))
661-
.Select(f => new AssemblyName { Name = Path.GetFileNameWithoutExtension(f) })
662-
.ToList();
645+
.Concat(new[] { Path.GetDirectoryName(typeof(VsTestRunner).Assembly.GetLocalCodeBase()) })
646+
.Distinct();
663647

664-
foreach (var assemblyName in depsAssms.Concat(dllsInSources))
648+
foreach (var folder in folders)
665649
{
666-
try
667-
{
668-
var assembly = Assembly.Load(assemblyName);
669-
foreach (var type in assembly.DefinedTypes)
670-
{
671-
#pragma warning disable CS0618
672-
if (type == null || type.IsAbstract || type == typeof(DefaultRunnerReporter).GetTypeInfo() || type == typeof(DefaultRunnerReporterWithTypes).GetTypeInfo() || type.ImplementedInterfaces.All(i => i != typeof(IRunnerReporter)))
673-
continue;
674-
#pragma warning restore CS0618
675-
676-
var ctor = type.DeclaredConstructors.FirstOrDefault(c => c.GetParameters().Length == 0);
677-
if (ctor == null)
678-
{
679-
ConsoleHelper.SetForegroundColor(ConsoleColor.Yellow);
680-
Console.WriteLine($"Type {type.FullName} in assembly {assembly} appears to be a runner reporter, but does not have an empty constructor.");
681-
ConsoleHelper.ResetColor();
682-
continue;
683-
}
650+
result.AddRange(RunnerReporterUtility.GetAvailableRunnerReporters(folder, out var messages));
684651

685-
result.Add((IRunnerReporter)ctor.Invoke(Array.Empty<object>()));
686-
}
687-
}
688-
catch
689-
{
690-
continue;
691-
}
652+
if (logger is not null)
653+
foreach (var message in messages)
654+
logger.LogWarning(message);
692655
}
693656

694657
return result;
695-
#else
696-
var result = new List<IRunnerReporter>();
697-
var runnerPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetLocalCodeBase());
698-
var runnerReporterInterfaceAssemblyFullName = typeof(IRunnerReporter).Assembly.GetName().FullName;
699-
700-
if (runnerPath != null)
701-
foreach (var dllFile in Directory.GetFiles(runnerPath, "*.dll").Select(f => Path.Combine(runnerPath, f)))
702-
{
703-
Type?[] types;
704-
705-
try
706-
{
707-
var assembly = Assembly.LoadFile(dllFile);
708-
709-
// Calling Assembly.GetTypes can be very expensive, while Assembly.GetReferencedAssemblies
710-
// is relatively cheap. We can avoid loading types for assemblies that couldn't possibly
711-
// reference IRunnerReporter.
712-
if (!assembly.GetReferencedAssemblies().Where(name => name.FullName == runnerReporterInterfaceAssemblyFullName).Any())
713-
continue;
714-
715-
types = assembly.GetTypes();
716-
}
717-
catch (ReflectionTypeLoadException ex)
718-
{
719-
types = ex.Types;
720-
}
721-
catch
722-
{
723-
continue;
724-
}
725-
726-
foreach (var type in types)
727-
{
728-
#pragma warning disable CS0618
729-
if (type == null || type.IsAbstract || type == typeof(DefaultRunnerReporter) || type == typeof(DefaultRunnerReporterWithTypes) || !type.GetInterfaces().Any(t => t == typeof(IRunnerReporter)))
730-
continue;
731-
#pragma warning restore CS0618
732-
733-
var ctor = type.GetConstructor(new Type[0]);
734-
if (ctor == null)
735-
{
736-
ConsoleHelper.SetForegroundColor(ConsoleColor.Yellow);
737-
Console.WriteLine($"Type {type.FullName} in assembly {dllFile} appears to be a runner reporter, but does not have an empty constructor.");
738-
ConsoleHelper.ResetColor();
739-
continue;
740-
}
741-
742-
result.Add((IRunnerReporter)ctor.Invoke(new object[0]));
743-
}
744-
}
745-
746-
return result;
747-
#endif
748658
}
749659

750660
static IList<DiscoveredTestCase> GetVsTestCases(
Lines changed: 14 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,58 @@
1-
using System.ComponentModel;
1+
using System;
22
using System.Diagnostics;
3-
using System.Reflection;
43
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
54
using NSubstitute;
65
using Xunit;
7-
using Xunit.Abstractions;
6+
using Xunit.Runner.Reporters;
87
using Xunit.Runner.VisualStudio;
98

109
public class RunnerReporterTests
1110
{
12-
public class TestRunnerReporterNotEnabled : IRunnerReporter
13-
{
14-
string IRunnerReporter.Description
15-
=> "Not auto-enabled runner";
16-
17-
bool IRunnerReporter.IsEnvironmentallyEnabled
18-
=> false;
19-
20-
string IRunnerReporter.RunnerSwitch
21-
=> "notautoenabled";
22-
23-
IMessageSink IRunnerReporter.CreateMessageHandler(IRunnerLogger logger)
24-
=> new NullMessageSink();
25-
}
26-
27-
public class TestRunnerReporter : TestRunnerReporterNotEnabled, IRunnerReporter
28-
{
29-
bool IRunnerReporter.IsEnvironmentallyEnabled
30-
=> true;
31-
32-
string IRunnerReporter.RunnerSwitch
33-
=> null;
34-
}
35-
3611
[Fact]
3712
public void WhenNotUsingAutoReporters_ChoosesDefault()
3813
{
14+
using var _ = EnvironmentHelper.NullifyEnvironmentalReporters();
3915
var settings = new RunSettings { NoAutoReporters = true };
4016

41-
var runnerReporter = VsTestRunner.GetRunnerReporter(null, settings, new[] { Assembly.GetExecutingAssembly().Location });
17+
var runnerReporter = VsTestRunner.GetRunnerReporter(null, settings, Array.Empty<string>());
4218

4319
Assert.Equal(typeof(DefaultRunnerReporterWithTypes).AssemblyQualifiedName, runnerReporter.GetType().AssemblyQualifiedName);
4420
}
4521

4622
[Fact]
4723
public void WhenUsingAutoReporters_DoesNotChooseDefault()
4824
{
25+
using var _ = EnvironmentHelper.NullifyEnvironmentalReporters();
26+
Environment.SetEnvironmentVariable("TEAMCITY_PROJECT_NAME", "foo"); // Force TeamCityReporter to surface environmentally
4927
var settings = new RunSettings { NoAutoReporters = false };
5028

51-
var runnerReporter = VsTestRunner.GetRunnerReporter(null, settings, new[] { Assembly.GetExecutingAssembly().Location });
29+
var runnerReporter = VsTestRunner.GetRunnerReporter(null, settings, Array.Empty<string>());
5230

53-
// We just make sure _an_ auto-reporter was chosen, but we can't rely on which one because this code
54-
// wil run when we're in CI, and therefore will choose the CI reporter sometimes. It's good enough
55-
// that we've provide an option above so that the default never gets chosen.
56-
Assert.NotEqual(typeof(DefaultRunnerReporterWithTypes).AssemblyQualifiedName, runnerReporter.GetType().AssemblyQualifiedName);
31+
Assert.Equal(typeof(TeamCityReporter).AssemblyQualifiedName, runnerReporter.GetType().AssemblyQualifiedName);
5732
}
5833

5934
[Fact]
6035
public void WhenUsingReporterSwitch_PicksThatReporter()
6136
{
62-
var settings = new RunSettings { NoAutoReporters = true, ReporterSwitch = "notautoenabled" };
37+
using var _ = EnvironmentHelper.NullifyEnvironmentalReporters();
38+
var settings = new RunSettings { NoAutoReporters = true, ReporterSwitch = "json" };
6339

64-
var runnerReporter = VsTestRunner.GetRunnerReporter(null, settings, new[] { Assembly.GetExecutingAssembly().Location });
40+
var runnerReporter = VsTestRunner.GetRunnerReporter(null, settings, Array.Empty<string>());
6541

66-
Assert.Equal(typeof(TestRunnerReporterNotEnabled).AssemblyQualifiedName, runnerReporter.GetType().AssemblyQualifiedName);
42+
Assert.Equal(typeof(JsonReporter).AssemblyQualifiedName, runnerReporter.GetType().AssemblyQualifiedName);
6743
}
6844

6945
[Fact]
7046
public void WhenRequestedReporterDoesntExist_LogsAndFallsBack()
7147
{
48+
using var _ = EnvironmentHelper.NullifyEnvironmentalReporters();
7249
var settings = new RunSettings { NoAutoReporters = true, ReporterSwitch = "thisnotavalidreporter" };
7350
var logger = Substitute.For<IMessageLogger>();
7451
var loggerHelper = new LoggerHelper(logger, new Stopwatch());
7552

76-
var runnerReporter = VsTestRunner.GetRunnerReporter(loggerHelper, settings, new[] { Assembly.GetExecutingAssembly().Location });
53+
var runnerReporter = VsTestRunner.GetRunnerReporter(loggerHelper, settings, Array.Empty<string>());
7754

7855
Assert.Equal(typeof(DefaultRunnerReporterWithTypes).AssemblyQualifiedName, runnerReporter.GetType().AssemblyQualifiedName);
7956
logger.Received(1).SendMessage(TestMessageLevel.Warning, "[xUnit.net 00:00:00.00] Could not find requested reporter 'thisnotavalidreporter'");
8057
}
81-
82-
[Fact]
83-
public void VSTestRunnerShouldHaveCategoryAttribute_WithValueManaged()
84-
{
85-
var attribute = typeof(VsTestRunner).GetCustomAttribute(typeof(CategoryAttribute));
86-
Assert.NotNull(attribute);
87-
Assert.Equal("managed", (attribute as CategoryAttribute)?.Category);
88-
}
8958
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
6+
static class EnvironmentHelper
7+
{
8+
static readonly string[] reporterEnvironmentVariables =
9+
{
10+
// AppVeyorReporter
11+
"APPVEYOR_API_URL",
12+
// TeamCityReporter
13+
"TEAMCITY_PROJECT_NAME",
14+
"TEAMCITY_PROCESS_FLOW_ID",
15+
// VstsReporter
16+
"VSTS_ACCESS_TOKEN",
17+
"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",
18+
"SYSTEM_TEAMPROJECT",
19+
"BUILD_BUILDID",
20+
};
21+
22+
public static IDisposable NullifyEnvironmentalReporters()
23+
{
24+
var result = new EnvironmentRestorer(reporterEnvironmentVariables);
25+
26+
foreach (var variable in reporterEnvironmentVariables)
27+
Environment.SetEnvironmentVariable(variable, null);
28+
29+
return result;
30+
}
31+
32+
public static IDisposable RestoreEnvironment(params string[] variables) =>
33+
new EnvironmentRestorer(variables);
34+
35+
class EnvironmentRestorer : IDisposable
36+
{
37+
Dictionary<string, string?> savedVariables = new();
38+
39+
public EnvironmentRestorer(string[] variables)
40+
{
41+
foreach (var variable in variables)
42+
savedVariables[variable] = Environment.GetEnvironmentVariable(variable);
43+
}
44+
45+
public void Dispose()
46+
{
47+
foreach (var kvp in savedVariables)
48+
Environment.SetEnvironmentVariable(kvp.Key, kvp.Value);
49+
}
50+
}
51+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.ComponentModel;
2+
using System.Reflection;
3+
using Xunit;
4+
using Xunit.Runner.VisualStudio;
5+
6+
public class VsTestRunnerTests
7+
{
8+
9+
[Fact]
10+
public void VSTestRunnerShouldHaveCategoryAttribute_WithValueManaged()
11+
{
12+
var attribute = typeof(VsTestRunner).GetCustomAttribute(typeof(CategoryAttribute));
13+
14+
Assert.NotNull(attribute);
15+
Assert.Equal("managed", (attribute as CategoryAttribute)?.Category);
16+
}
17+
}

0 commit comments

Comments
 (0)