diff --git a/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs b/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs index 5712480a8..d6c1091a8 100644 --- a/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs +++ b/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs @@ -98,7 +98,7 @@ await TestHelper.DelayUntil( client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == "@/" + TextDocumentNames.SemanticTokensFull); } - [FactWithSkipOn(SkipOnPlatform.All)] + [RetryFact] public async Task Should_Unregister_Dynamically_While_Server_Is_Running() { var (client, server) = await Initialize(new ConfigureClient().Configure, new ConfigureServer().Configure); diff --git a/test/Lsp.Tests/Integration/PartialItemTests.cs b/test/Lsp.Tests/Integration/PartialItemTests.cs index bd6dd4c7b..9ad4b2c2c 100644 --- a/test/Lsp.Tests/Integration/PartialItemTests.cs +++ b/test/Lsp.Tests/Integration/PartialItemTests.cs @@ -24,7 +24,7 @@ public Delegates(ITestOutputHelper testOutputHelper, LanguageProtocolFixture z.Command!.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); } - [FactWithSkipOn(SkipOnPlatform.All)] + [RetryFact] public async Task Should_Behave_Like_An_Observable() { var items = await Client.TextDocument diff --git a/test/Lsp.Tests/Integration/TypedCodeActionTests.cs b/test/Lsp.Tests/Integration/TypedCodeActionTests.cs index ce657ac18..513bc96f5 100644 --- a/test/Lsp.Tests/Integration/TypedCodeActionTests.cs +++ b/test/Lsp.Tests/Integration/TypedCodeActionTests.cs @@ -204,7 +204,7 @@ public async Task Should_Resolve_With_Data_Capability() item.CodeAction!.Command!.Name.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_Capability() { var (client, _) = await Initialize( @@ -297,7 +297,7 @@ public async Task Should_Resolve_With_Data_CancellationToken() item.CodeAction!.Command!.Name.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_CancellationToken() { var (client, _) = await Initialize( diff --git a/test/Lsp.Tests/Integration/TypedCodeLensTests.cs b/test/Lsp.Tests/Integration/TypedCodeLensTests.cs index 06a4b20ea..5dee833ab 100644 --- a/test/Lsp.Tests/Integration/TypedCodeLensTests.cs +++ b/test/Lsp.Tests/Integration/TypedCodeLensTests.cs @@ -195,7 +195,7 @@ public async Task Should_Resolve_With_Data_Capability() item.Command!.Name.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_Capability() { var (client, _) = await Initialize( @@ -284,7 +284,7 @@ public async Task Should_Resolve_With_Data_CancellationToken() item.Command!.Name.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_CancellationToken() { var (client, _) = await Initialize( diff --git a/test/Lsp.Tests/Integration/TypedCompletionTests.cs b/test/Lsp.Tests/Integration/TypedCompletionTests.cs index aa2de3946..13fd6a3f7 100644 --- a/test/Lsp.Tests/Integration/TypedCompletionTests.cs +++ b/test/Lsp.Tests/Integration/TypedCompletionTests.cs @@ -195,7 +195,7 @@ public async Task Should_Resolve_With_Data_Capability() item.Detail.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_Capability() { var (client, _) = await Initialize( @@ -284,7 +284,7 @@ public async Task Should_Resolve_With_Data_CancellationToken() item.Detail.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_CancellationToken() { var (client, _) = await Initialize( diff --git a/test/Lsp.Tests/Integration/TypedDocumentLinkTests.cs b/test/Lsp.Tests/Integration/TypedDocumentLinkTests.cs index ad6b5b36b..ef686eefc 100644 --- a/test/Lsp.Tests/Integration/TypedDocumentLinkTests.cs +++ b/test/Lsp.Tests/Integration/TypedDocumentLinkTests.cs @@ -178,7 +178,7 @@ public async Task Should_Resolve_With_Data_Capability() item.Tooltip.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_Capability() { var (client, _) = await Initialize( @@ -261,7 +261,7 @@ public async Task Should_Resolve_With_Data_CancellationToken() item.Tooltip.Should().Be("resolved"); } - [FactWithSkipOn(SkipOnPlatform.Mac)] + [RetryFact] public async Task Should_Resolve_With_Partial_Data_CancellationToken() { var (client, _) = await Initialize( diff --git a/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs b/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs index 2d9d844a4..dac9b8c0a 100644 --- a/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs +++ b/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs @@ -46,7 +46,7 @@ public void Swap(ITestOutputHelper testOutputHelper) _testOutputHelper.Swap(testOutputHelper); } - class InnerTestOutputHelper : ITestOutputHelper + private class InnerTestOutputHelper : ITestOutputHelper { private ITestOutputHelper? _testOutputHelper; diff --git a/test/TestingUtils/BlockingMessageBus.cs b/test/TestingUtils/BlockingMessageBus.cs new file mode 100644 index 000000000..c82955dfb --- /dev/null +++ b/test/TestingUtils/BlockingMessageBus.cs @@ -0,0 +1,52 @@ +// See https://github.com/JoshKeegan/xRetry + +using System.Collections.Concurrent; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestingUtils +{ + /// + /// An XUnit message bus that can block messages from being passed until we want them to be. + /// + public class BlockingMessageBus : IMessageBus + { + private readonly IMessageBus _underlyingMessageBus; + private ConcurrentQueue _messageQueue = new ConcurrentQueue(); + + public BlockingMessageBus(IMessageBus underlyingMessageBus) + { + _underlyingMessageBus = underlyingMessageBus; + } + + public bool QueueMessage(IMessageSinkMessage message) + { + _messageQueue.Enqueue(message); + + // Returns if execution should continue. Since we are intercepting the message, we + // have no way of checking this so always continue... + return true; + } + + public void Clear() + { + _messageQueue = new ConcurrentQueue(); + } + + /// + /// Write the cached messages to the underlying message bus + /// + public void Flush() + { + while (_messageQueue.TryDequeue(out IMessageSinkMessage message)) + { + _underlyingMessageBus.QueueMessage(message); + } + } + + public void Dispose() + { + // Do not dispose of the underlying message bus - it is an externally owned resource + } + } +} diff --git a/test/TestingUtils/IRetryableTestCase.cs b/test/TestingUtils/IRetryableTestCase.cs new file mode 100644 index 000000000..2611319e7 --- /dev/null +++ b/test/TestingUtils/IRetryableTestCase.cs @@ -0,0 +1,12 @@ +// See https://github.com/JoshKeegan/xRetry + +using Xunit.Sdk; + +namespace TestingUtils +{ + public interface IRetryableTestCase : IXunitTestCase + { + int MaxRetries { get; } + int DelayBetweenRetriesMs { get; } + } +} diff --git a/test/TestingUtils/RetryFactAttribute.cs b/test/TestingUtils/RetryFactAttribute.cs new file mode 100644 index 000000000..ff825a0c5 --- /dev/null +++ b/test/TestingUtils/RetryFactAttribute.cs @@ -0,0 +1,40 @@ +// See https://github.com/JoshKeegan/xRetry + +using System; +using Xunit; +using Xunit.Sdk; + +namespace TestingUtils +{ + /// + /// Attribute that is applied to a method to indicate that it is a fact that should be run + /// by the test runner up to MaxRetries times, until it succeeds. + /// + [XunitTestCaseDiscoverer("xRetry.RetryFactDiscoverer", "xRetry")] + [AttributeUsage(AttributeTargets.Method)] + public class RetryFactAttribute : FactAttribute + { + public readonly int MaxRetries; + public readonly int DelayBetweenRetriesMs; + + /// + /// Ctor + /// + /// The number of times to run a test for until it succeeds + /// The amount of time (in ms) to wait between each test run attempt + public RetryFactAttribute(int maxRetries = 3, int delayBetweenRetriesMs = 0) + { + if (maxRetries < 1) + { + throw new ArgumentOutOfRangeException(nameof(maxRetries) + " must be >= 1"); + } + if (delayBetweenRetriesMs < 0) + { + throw new ArgumentOutOfRangeException(nameof(delayBetweenRetriesMs) + " must be >= 0"); + } + + MaxRetries = UnitTestDetector.IsCI() ? maxRetries : 1; + DelayBetweenRetriesMs = delayBetweenRetriesMs; + } + } +} diff --git a/test/TestingUtils/RetryFactDiscoverer.cs b/test/TestingUtils/RetryFactDiscoverer.cs new file mode 100644 index 000000000..7db21d8ff --- /dev/null +++ b/test/TestingUtils/RetryFactDiscoverer.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestingUtils +{ + public class RetryFactDiscoverer : IXunitTestCaseDiscoverer + { + private readonly IMessageSink _messageSink; + + public RetryFactDiscoverer(IMessageSink messageSink) + { + _messageSink = messageSink; + } + + public IEnumerable Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, + IAttributeInfo factAttribute) + { + IXunitTestCase testCase; + + if (testMethod.Method.GetParameters().Any()) + { + testCase = new ExecutionErrorTestCase(_messageSink, discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, + "[RetryFact] methods are not allowed to have parameters. Did you mean to use [RetryTheory]?"); + } + else if (testMethod.Method.IsGenericMethodDefinition) + { + testCase = new ExecutionErrorTestCase(_messageSink, discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, + "[RetryFact] methods are not allowed to be generic."); + } + else + { + var maxRetries = factAttribute.GetNamedArgument(nameof(RetryFactAttribute.MaxRetries)); + var delayBetweenRetriesMs = + factAttribute.GetNamedArgument(nameof(RetryFactAttribute.DelayBetweenRetriesMs)); + testCase = new RetryTestCase(_messageSink, discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries, delayBetweenRetriesMs); + } + + return new[] { testCase }; + } + } +} diff --git a/test/TestingUtils/RetryTestCase.cs b/test/TestingUtils/RetryTestCase.cs new file mode 100644 index 000000000..53ae6f395 --- /dev/null +++ b/test/TestingUtils/RetryTestCase.cs @@ -0,0 +1,60 @@ +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestingUtils +{ + [Serializable] + public class RetryTestCase : XunitTestCase, IRetryableTestCase + { + public int MaxRetries { get; private set; } + public int DelayBetweenRetriesMs { get; private set; } + + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete( + "Called by the de-serializer; should only be called by deriving classes for de-serialization purposes", true)] + public RetryTestCase() { } + + public RetryTestCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + int maxRetries, + int delayBetweenRetriesMs, + object[]? testMethodArguments = null) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod, + testMethodArguments) + { + MaxRetries = maxRetries; + DelayBetweenRetriesMs = delayBetweenRetriesMs; + } + + public override Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, + object[] constructorArguments, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) => + RetryTestCaseRunner.RunAsync(this, diagnosticMessageSink, messageBus, cancellationTokenSource, + blockingMessageBus => new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, + TestMethodArguments, blockingMessageBus, aggregator, cancellationTokenSource) + .RunAsync()); + + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + + data.AddValue("MaxRetries", MaxRetries); + data.AddValue("DelayBetweenRetriesMs", DelayBetweenRetriesMs); + } + + public override void Deserialize(IXunitSerializationInfo data) + { + base.Deserialize(data); + + MaxRetries = data.GetValue("MaxRetries"); + DelayBetweenRetriesMs = data.GetValue("DelayBetweenRetriesMs"); + } + } +} diff --git a/test/TestingUtils/RetryTestCaseRunner.cs b/test/TestingUtils/RetryTestCaseRunner.cs new file mode 100644 index 000000000..8cfa36a03 --- /dev/null +++ b/test/TestingUtils/RetryTestCaseRunner.cs @@ -0,0 +1,76 @@ +// See https://github.com/JoshKeegan/xRetry + +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestingUtils +{ + internal static class RetryTestCaseRunner + { + /// + /// Runs a retryable test case, handling any wait and retry logic between test runs, reporting statuses out to xunit etc... + /// + /// The test case to be retried + /// The diagnostic message sink to write messages to about retries, waits etc... + /// The message bus xunit is listening for statuses to report on + /// The cancellation token source from xunit + /// (async) Lambda to run this test case once (without retries) - takes the blocking message bus and returns the test run result + /// Resulting run summary + public static async Task RunAsync( + IRetryableTestCase testCase, + IMessageSink diagnosticMessageSink, + IMessageBus messageBus, + CancellationTokenSource cancellationTokenSource, + Func> fnRunSingle) + { + for (var i = 1; ; i++) + { + // Prevent messages from the test run from being passed through, as we don't want + // a message to mark the test as failed when we're going to retry it + using BlockingMessageBus blockingMessageBus = new BlockingMessageBus(messageBus); + diagnosticMessageSink.OnMessage(new DiagnosticMessage("Running test \"{0}\" attempt ({1}/{2})", + testCase.DisplayName, i, testCase.MaxRetries)); + + RunSummary summary = await fnRunSingle(blockingMessageBus); + + // If we succeeded, or we've reached the max retries return the result + if (summary.Failed == 0 || i == testCase.MaxRetries) + { + // If we have failed (after all retries, log that) + if (summary.Failed != 0) + { + diagnosticMessageSink.OnMessage(new DiagnosticMessage( + "Test \"{0}\" has failed and been retried the maximum number of times ({1})", + testCase.DisplayName, testCase.MaxRetries)); + } + + blockingMessageBus.Flush(); + return summary; + } + // Otherwise log that we've had a failed run and will retry + diagnosticMessageSink.OnMessage(new DiagnosticMessage( + "Test \"{0}\" failed but is set to retry ({1}/{2}) . . .", testCase.DisplayName, i, + testCase.MaxRetries)); + + // If there is a delay between test attempts, apply it now + if (testCase.DelayBetweenRetriesMs > 0) + { + diagnosticMessageSink.OnMessage(new DiagnosticMessage( + "Test \"{0}\" attempt ({1}/{2}) delayed by {3}ms. Waiting . . .", testCase.DisplayName, i, + testCase.MaxRetries, testCase.DelayBetweenRetriesMs)); + + // Don't await to prevent thread hopping. + // If all of a users test cases in a collection/class are synchronous and expecting to not thread-hop + // (because they're making use of thread static/thread local/managed thread ID to share data between tests rather than + // a more modern async-friendly mechanism) then if a thread-hop were to happen here we'd get flickering tests. + // SpecFlow relies on this as they use the managed thread ID to separate instances of some of their internal classes, which caused + // a this problem for xRetry.SpecFlow: https://github.com/JoshKeegan/xRetry/issues/18 + Task.Delay(testCase.DelayBetweenRetriesMs, cancellationTokenSource.Token).Wait(); + } + } + } + } +} diff --git a/test/TestingUtils/RetryTheoryAttribute.cs b/test/TestingUtils/RetryTheoryAttribute.cs new file mode 100644 index 000000000..93ec88d75 --- /dev/null +++ b/test/TestingUtils/RetryTheoryAttribute.cs @@ -0,0 +1,20 @@ +// See https://github.com/JoshKeegan/xRetry + +using System; +using Xunit.Sdk; + +namespace TestingUtils +{ + /// + /// Attribute that is applied to a method to indicate that it is a theory that should be run + /// by the test runner up to MaxRetries times, until it succeeds. + /// + [XunitTestCaseDiscoverer("xRetry.RetryTheoryDiscoverer", "xRetry")] + [AttributeUsage(AttributeTargets.Method)] + public class RetryTheoryAttribute : RetryFactAttribute + { + /// + public RetryTheoryAttribute(int maxRetries = 3, int delayBetweenRetriesMs = 0) + : base(maxRetries, delayBetweenRetriesMs) { } + } +} diff --git a/test/TestingUtils/RetryTheoryDiscoverer.cs b/test/TestingUtils/RetryTheoryDiscoverer.cs new file mode 100644 index 000000000..263ee06ce --- /dev/null +++ b/test/TestingUtils/RetryTheoryDiscoverer.cs @@ -0,0 +1,50 @@ +// See https://github.com/JoshKeegan/xRetry + +using System.Collections.Generic; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestingUtils +{ + public class RetryTheoryDiscoverer : TheoryDiscoverer + { + public RetryTheoryDiscoverer(IMessageSink diagnosticMessageSink) + : base(diagnosticMessageSink) { } + + protected override IEnumerable CreateTestCasesForDataRow( + ITestFrameworkDiscoveryOptions discoveryOptions, + ITestMethod testMethod, + IAttributeInfo theoryAttribute, + object[] dataRow) + { + var maxRetries = theoryAttribute.GetNamedArgument(nameof(RetryTheoryAttribute.MaxRetries)); + var delayBetweenRetriesMs = + theoryAttribute.GetNamedArgument(nameof(RetryTheoryAttribute.DelayBetweenRetriesMs)); + return new[] + { + new RetryTestCase( + DiagnosticMessageSink, + discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), + testMethod, + maxRetries, + delayBetweenRetriesMs, + dataRow) + }; + } + + protected override IEnumerable CreateTestCasesForTheory( + ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo theoryAttribute) + { + var maxRetries = theoryAttribute.GetNamedArgument(nameof(RetryTheoryAttribute.MaxRetries)); + var delayBetweenRetriesMs = + theoryAttribute.GetNamedArgument(nameof(RetryTheoryAttribute.DelayBetweenRetriesMs)); + + return new[] + { + new RetryTheoryDiscoveryAtRuntimeCase(DiagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), + discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries, delayBetweenRetriesMs) + }; + } + } +} diff --git a/test/TestingUtils/RetryTheoryDiscoveryAtRuntimeCase.cs b/test/TestingUtils/RetryTheoryDiscoveryAtRuntimeCase.cs new file mode 100644 index 000000000..4fd4df6a0 --- /dev/null +++ b/test/TestingUtils/RetryTheoryDiscoveryAtRuntimeCase.cs @@ -0,0 +1,66 @@ +// See https://github.com/JoshKeegan/xRetry + +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Xunit.Abstractions; +using Xunit.Sdk; + +namespace TestingUtils +{ + /// + /// Represents a test case to be retried which runs multiple tests for theory data, either because the + /// data was not enumerable or because the data was not serializable. + /// Equivalent to xunit's XunitTheoryTestCase + /// + [Serializable] + public class RetryTheoryDiscoveryAtRuntimeCase : XunitTestCase, IRetryableTestCase + { + public int MaxRetries { get; private set; } + public int DelayBetweenRetriesMs { get; private set; } + + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Called by the de-serializer; should only be called by deriving classes for de-serialization purposes")] + public RetryTheoryDiscoveryAtRuntimeCase() { } + + public RetryTheoryDiscoveryAtRuntimeCase( + IMessageSink diagnosticMessageSink, + TestMethodDisplay defaultMethodDisplay, + TestMethodDisplayOptions defaultMethodDisplayOptions, + ITestMethod testMethod, + int maxRetries, + int delayBetweenRetriesMs) + : base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod) + { + MaxRetries = maxRetries; + DelayBetweenRetriesMs = delayBetweenRetriesMs; + } + + /// + public override Task RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus, + object[] constructorArguments, ExceptionAggregator aggregator, + CancellationTokenSource cancellationTokenSource) => + RetryTestCaseRunner.RunAsync(this, diagnosticMessageSink, messageBus, cancellationTokenSource, + blockingMessageBus => new XunitTheoryTestCaseRunner(this, DisplayName, SkipReason, constructorArguments, + diagnosticMessageSink, blockingMessageBus, aggregator, cancellationTokenSource) + .RunAsync()); + + public override void Serialize(IXunitSerializationInfo data) + { + base.Serialize(data); + + data.AddValue("MaxRetries", MaxRetries); + data.AddValue("DelayBetweenRetriesMs", DelayBetweenRetriesMs); + } + + public override void Deserialize(IXunitSerializationInfo data) + { + base.Deserialize(data); + + MaxRetries = data.GetValue("MaxRetries"); + DelayBetweenRetriesMs = data.GetValue("DelayBetweenRetriesMs"); + } + } +} diff --git a/test/TestingUtils/TheoryWithSkipOnAttribute.cs b/test/TestingUtils/TheoryWithSkipOnAttribute.cs index 8fd6feae6..1b1609da3 100644 --- a/test/TestingUtils/TheoryWithSkipOnAttribute.cs +++ b/test/TestingUtils/TheoryWithSkipOnAttribute.cs @@ -15,7 +15,7 @@ public TheoryWithSkipOnAttribute(params SkipOnPlatform[] platformsToSkip) public override string? Skip { - get => /*!UnitTestDetector.IsCI() && */_platformsToSkip.Any(UnitTestDetector.PlatformToSkipPredicate) + get => !UnitTestDetector.IsCI() && _platformsToSkip.Any(UnitTestDetector.PlatformToSkipPredicate) ? "Skipped on platform" + ( string.IsNullOrWhiteSpace(_skip) ? "" : " because " + _skip ) : null; set => _skip = value; diff --git a/test/TestingUtils/UnitTestDetector.cs b/test/TestingUtils/UnitTestDetector.cs index 5a0c5abf6..6cf1df25e 100644 --- a/test/TestingUtils/UnitTestDetector.cs +++ b/test/TestingUtils/UnitTestDetector.cs @@ -4,7 +4,7 @@ namespace TestingUtils { - class UnitTestDetector + internal class UnitTestDetector { // ReSharper disable once InconsistentNaming public static bool IsCI() => string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CI"))