Skip to content

Commit 3ddfa0f

Browse files
Fixed schedulers for replay subjects
1 parent 0dae323 commit 3ddfa0f

20 files changed

+401
-20
lines changed

src/Client/LanguageClientRegistrationManager.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Reactive.Concurrency;
56
using System.Reactive.Linq;
67
using System.Reactive.Subjects;
78
using System.Threading;
@@ -26,7 +27,7 @@ internal class LanguageClientRegistrationManager : IRegisterCapabilityHandler, I
2627
private readonly ILspHandlerTypeDescriptorProvider _handlerTypeDescriptorProvider;
2728
private readonly ILogger<LanguageClientRegistrationManager> _logger;
2829
private readonly ConcurrentDictionary<string, Registration> _registrations;
29-
private readonly ReplaySubject<IEnumerable<Registration>> _registrationSubject = new ReplaySubject<IEnumerable<Registration>>(1);
30+
private readonly ReplaySubject<IEnumerable<Registration>> _registrationSubject = new ReplaySubject<IEnumerable<Registration>>(1, Scheduler.Immediate);
3031

3132
public LanguageClientRegistrationManager(
3233
ISerializer serializer,

src/Client/LanguageClientWorkspaceFoldersManager.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Reactive.Concurrency;
56
using System.Reactive.Linq;
67
using System.Reactive.Subjects;
78
using System.Threading;
@@ -25,7 +26,7 @@ public LanguageClientWorkspaceFoldersManager(IWorkspaceLanguageClient client, IE
2526
{
2627
_client = client;
2728
_workspaceFolders = new ConcurrentDictionary<DocumentUri, WorkspaceFolder>(DocumentUri.Comparer);
28-
_workspaceFoldersSubject = new ReplaySubject<IEnumerable<WorkspaceFolder>>(1);
29+
_workspaceFoldersSubject = new ReplaySubject<IEnumerable<WorkspaceFolder>>(1, Scheduler.Immediate);
2930

3031
foreach (var folder in workspaceFolders)
3132
{

src/Dap.Client/ProgressObservable.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reactive.Concurrency;
23
using System.Reactive.Disposables;
34
using System.Reactive.Subjects;
45
using OmniSharp.Extensions.DebugAdapter.Protocol;
@@ -14,7 +15,7 @@ internal class ProgressObservable : IProgressObservable, IObserver<ProgressEvent
1415

1516
public ProgressObservable(ProgressToken token)
1617
{
17-
_dataSubject = new ReplaySubject<ProgressEvent>(1);
18+
_dataSubject = new ReplaySubject<ProgressEvent>(1, Scheduler.Immediate);
1819
_disposable = new CompositeDisposable { Disposable.Create(_dataSubject.OnCompleted) };
1920

2021
ProgressToken = token;

src/JsonRpc/OutputHandler.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ ILogger<OutputHandler> logger
3939
_outputFilters = outputFilters.ToArray();
4040
_logger = logger;
4141
_queue = new Subject<object>();
42-
_delayedQueue = new ReplaySubject<object>();
42+
_delayedQueue = new ReplaySubject<object>(Scheduler.Immediate);
4343
_outputIsFinished = new TaskCompletionSource<object?>();
4444

4545
_disposable = new CompositeDisposable {

src/JsonRpc/ProcessScheduler.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ IScheduler scheduler
3434

3535
var observableQueue =
3636
new BehaviorSubject<(RequestProcessType type, ReplaySubject<IObservable<Unit>> observer, Subject<Unit>? contentModifiedSource)>(
37-
( RequestProcessType.Serial, new ReplaySubject<IObservable<Unit>>(int.MaxValue), supportContentModified ? new Subject<Unit>() : null )
37+
( RequestProcessType.Serial, new ReplaySubject<IObservable<Unit>>(int.MaxValue, Scheduler.Immediate), supportContentModified ? new Subject<Unit>() : null )
3838
);
3939

4040
cd.Add(
@@ -52,7 +52,7 @@ IScheduler scheduler
5252

5353
logger.LogDebug("Completing existing request process type {Type}", observableQueue.Value.type);
5454
observableQueue.Value.observer.OnCompleted();
55-
observableQueue.OnNext(( item.type, new ReplaySubject<IObservable<Unit>>(int.MaxValue), supportContentModified ? new Subject<Unit>() : null ));
55+
observableQueue.OnNext(( item.type, new ReplaySubject<IObservable<Unit>>(int.MaxValue, Scheduler.Immediate), supportContentModified ? new Subject<Unit>() : null ));
5656
}
5757

5858
logger.LogDebug("Queueing {Type}:{Name} request for processing", item.type, item.name);

src/Protocol/Progress/PartialItemsRequestProgressObservable.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Action onCompleteAction
3636
)
3737
{
3838
_serializer = serializer;
39-
_dataSubject = new ReplaySubject<IEnumerable<TItem>>(int.MaxValue);
39+
_dataSubject = new ReplaySubject<IEnumerable<TItem>>(int.MaxValue, Scheduler.Immediate);
4040
_disposable = new CompositeDisposable() { _dataSubject };
4141

4242
_task = Observable.Create<TResult>(

src/Protocol/Progress/ProgressObservable.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reactive.Concurrency;
23
using System.Reactive.Disposables;
34
using System.Reactive.Linq;
45
using System.Reactive.Subjects;
@@ -16,7 +17,7 @@ internal class ProgressObservable<T> : IProgressObservable<T>, IObserver<JToken>
1617
public ProgressObservable(ProgressToken token, Func<JToken, T> factory, Action disposal)
1718
{
1819
_factory = factory;
19-
_dataSubject = new ReplaySubject<JToken>(1);
20+
_dataSubject = new ReplaySubject<JToken>(1, Scheduler.Immediate);
2021
_disposable = new CompositeDisposable { Disposable.Create(_dataSubject.OnCompleted), Disposable.Create(disposal) };
2122

2223
ProgressToken = token;

src/Server/LanguageServerWorkspaceFolderManager.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
44
using System.Linq;
5+
using System.Reactive.Concurrency;
56
using System.Reactive.Linq;
67
using System.Reactive.Subjects;
78
using System.Threading;
@@ -27,7 +28,7 @@ public LanguageServerWorkspaceFolderManager(IWorkspaceLanguageServer server)
2728
{
2829
_server = server;
2930
_workspaceFolders = new ConcurrentDictionary<DocumentUri, WorkspaceFolder>(DocumentUri.Comparer);
30-
_workspaceFoldersSubject = new ReplaySubject<IEnumerable<WorkspaceFolder>>(1);
31+
_workspaceFoldersSubject = new ReplaySubject<IEnumerable<WorkspaceFolder>>(1, Scheduler.Immediate);
3132
_workspaceFoldersChangedSubject = new Subject<WorkspaceFolderChange>();
3233
}
3334

test/Lsp.Tests/Integration/DynamicRegistrationTests.cs

+3
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,8 @@ await TestHelper.DelayUntil(
119119
registrations => registrations.Any(registration => SelectorMatches(registration, x => x.HasLanguage && x.Language == "vb")),
120120
CancellationToken
121121
);
122+
123+
await Task.Delay(200);
122124
disposable.Dispose();
123125

124126

@@ -127,6 +129,7 @@ await TestHelper.DelayUntil(
127129
registrations => !registrations.Any(registration => SelectorMatches(registration, x => x.HasLanguage && x.Language == "vb")),
128130
CancellationToken
129131
);
132+
await Task.Delay(200);
130133

131134
client.RegistrationManager.CurrentRegistrations.Should().NotContain(
132135
x =>

test/Lsp.Tests/Integration/LanguageServerConfigurationTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,15 @@ public async Task Should_Support_Options_Monitor()
254254
// IOptionsMonitor<> is registered as a singleton, so this will update
255255
options.CurrentValue.Host.Should().Be("localhost");
256256
options.CurrentValue.Port.Should().Be(443);
257-
sub.Received(1).Invoke(Arg.Any<BinderSourceUrl>());
257+
sub.Received(Quantity.AtLeastOne()).Invoke(Arg.Any<BinderSourceUrl>());
258258

259259
configuration.Update("mysection", new Dictionary<string, string> { ["host"] = "127.0.0.1", ["port"] = "80" });
260260
await options.WaitForChange(CancellationToken);
261261
await SettleNext();
262262

263263
options.CurrentValue.Host.Should().Be("127.0.0.1");
264264
options.CurrentValue.Port.Should().Be(80);
265-
sub.Received(2).Invoke(Arg.Any<BinderSourceUrl>());
265+
sub.Received(Quantity.Within(2, int.MaxValue)).Invoke(Arg.Any<BinderSourceUrl>());
266266
}
267267

268268
class BinderSourceUrl

test/Lsp.Tests/Integration/PartialItemTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public Delegates(ITestOutputHelper testOutputHelper, LanguageProtocolFixture<Def
2424
{
2525
}
2626

27-
[Fact]
27+
[RetryFact]
2828
public async Task Should_Behave_Like_A_Task()
2929
{
3030
var result = await Client.TextDocument.RequestSemanticTokens(
@@ -34,7 +34,7 @@ public async Task Should_Behave_Like_A_Task()
3434
result!.Data.Should().HaveCount(3);
3535
}
3636

37-
[Fact]
37+
[RetryFact]
3838
public async Task Should_Behave_Like_An_Observable()
3939
{
4040
var items = await Client.TextDocument
@@ -53,7 +53,7 @@ public async Task Should_Behave_Like_An_Observable()
5353
items.Select(z => z.Data.Length).Should().ContainInOrder(1, 2, 3);
5454
}
5555

56-
[Fact]
56+
[RetryFact]
5757
public async Task Should_Behave_Like_An_Observable_Without_Progress_Support()
5858
{
5959
var response = await Client.SendRequest(new SemanticTokensParams { TextDocument = new TextDocumentIdentifier(@"c:\test.cs") }, CancellationToken);

test/Lsp.Tests/Integration/PartialItemsTests.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public Delegates(ITestOutputHelper testOutputHelper, LanguageProtocolFixture<Def
2929
{
3030
}
3131

32-
[Fact]
32+
[RetryFact]
3333
public async Task Should_Behave_Like_A_Task()
3434
{
3535
var result = await Client.TextDocument.RequestCodeLens(
@@ -42,7 +42,7 @@ public async Task Should_Behave_Like_A_Task()
4242
result.Select(z => z.Command!.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3");
4343
}
4444

45-
[Fact]
45+
[RetryFact]
4646
public async Task Should_Behave_Like_An_Observable()
4747
{
4848
var items = await Client.TextDocument
@@ -63,7 +63,7 @@ public async Task Should_Behave_Like_An_Observable()
6363
items.Select(z => z.Command!.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3");
6464
}
6565

66-
[Fact]
66+
[RetryFact]
6767
public async Task Should_Behave_Like_An_Observable_Without_Progress_Support()
6868
{
6969
var response = await Client.SendRequest(
@@ -120,7 +120,7 @@ public Handlers(ITestOutputHelper testOutputHelper, LanguageProtocolFixture<Defa
120120
{
121121
}
122122

123-
[Fact]
123+
[RetryFact]
124124
public async Task Should_Behave_Like_An_Observable_With_WorkDone()
125125
{
126126
var items = new List<CodeLens>();

test/TestingUtils/AutoNSubstitute/TestExtensions.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
using System.Reactive.Concurrency;
2-
using System.Threading;
1+
using System.Threading;
32
using OmniSharp.Extensions.JsonRpc.Testing;
43
using Serilog.Events;
54
using Xunit.Abstractions;
+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// See https://github.com/JoshKeegan/xRetry
2+
3+
using System;
4+
using System.Linq;
5+
using Xunit;
6+
using Xunit.Sdk;
7+
8+
namespace TestingUtils
9+
{
10+
/// <summary>
11+
/// Attribute that is applied to a method to indicate that it is a fact that should be run
12+
/// by the test runner up to MaxRetries times, until it succeeds.
13+
/// </summary>
14+
[XunitTestCaseDiscoverer("TestingUtils.RetryFactDiscoverer", "TestingUtils")]
15+
[AttributeUsage(AttributeTargets.Method)]
16+
public class RetryFactAttribute : FactAttribute
17+
{
18+
public readonly int MaxRetries;
19+
public readonly int DelayBetweenRetriesMs;
20+
public readonly SkipOnPlatform[] PlatformsToSkip;
21+
private string? _skip;
22+
23+
/// <summary>
24+
/// Ctor
25+
/// </summary>
26+
/// <param name="maxRetries">The number of times to run a test for until it succeeds</param>
27+
/// <param name="delayBetweenRetriesMs">The amount of time (in ms) to wait between each test run attempt</param>
28+
/// <param name="skipOn">platforms to skip testing on</param>
29+
public RetryFactAttribute(int maxRetries = 5, int delayBetweenRetriesMs = 0, params SkipOnPlatform[] skipOn)
30+
{
31+
if (maxRetries < 1)
32+
{
33+
throw new ArgumentOutOfRangeException(nameof(maxRetries) + " must be >= 1");
34+
}
35+
if (delayBetweenRetriesMs < 0)
36+
{
37+
throw new ArgumentOutOfRangeException(nameof(delayBetweenRetriesMs) + " must be >= 0");
38+
}
39+
40+
MaxRetries = !UnitTestDetector.IsCI() ? 1 : maxRetries;
41+
DelayBetweenRetriesMs = delayBetweenRetriesMs;
42+
PlatformsToSkip = skipOn;
43+
}
44+
45+
public override string? Skip
46+
{
47+
get => UnitTestDetector.IsCI() && PlatformsToSkip.Any(UnitTestDetector.PlatformToSkipPredicate)
48+
? "Skipped on platform" + ( string.IsNullOrWhiteSpace(_skip) ? "" : " because " + _skip )
49+
: null;
50+
set => _skip = value;
51+
}
52+
}
53+
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Xunit.Abstractions;
4+
using Xunit.Sdk;
5+
6+
namespace TestingUtils
7+
{
8+
public class RetryFactDiscoverer : IXunitTestCaseDiscoverer
9+
{
10+
private readonly IMessageSink _messageSink;
11+
12+
public RetryFactDiscoverer(IMessageSink messageSink)
13+
{
14+
_messageSink = messageSink;
15+
}
16+
17+
public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod,
18+
IAttributeInfo factAttribute)
19+
{
20+
IXunitTestCase testCase;
21+
22+
if (testMethod.Method.GetParameters().Any())
23+
{
24+
testCase = new ExecutionErrorTestCase(_messageSink, discoveryOptions.MethodDisplayOrDefault(),
25+
discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod,
26+
"[RetryFact] methods are not allowed to have parameters. Did you mean to use [RetryTheory]?");
27+
}
28+
else if (testMethod.Method.IsGenericMethodDefinition)
29+
{
30+
testCase = new ExecutionErrorTestCase(_messageSink, discoveryOptions.MethodDisplayOrDefault(),
31+
discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod,
32+
"[RetryFact] methods are not allowed to be generic.");
33+
}
34+
else
35+
{
36+
var maxRetries = factAttribute.GetNamedArgument<int>(nameof(RetryFactAttribute.MaxRetries));
37+
var delayBetweenRetriesMs =
38+
factAttribute.GetNamedArgument<int>(nameof(RetryFactAttribute.DelayBetweenRetriesMs));
39+
testCase = new RetryTestCase(_messageSink, discoveryOptions.MethodDisplayOrDefault(),
40+
discoveryOptions.MethodDisplayOptionsOrDefault(), testMethod, maxRetries, delayBetweenRetriesMs);
41+
}
42+
43+
return new[] { testCase };
44+
}
45+
}
46+
}

test/TestingUtils/RetryTestCase.cs

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Xunit.Abstractions;
6+
using Xunit.Sdk;
7+
8+
namespace TestingUtils
9+
{
10+
[Serializable]
11+
public class RetryTestCase : XunitTestCase, IRetryableTestCase
12+
{
13+
public int MaxRetries { get; private set; }
14+
public int DelayBetweenRetriesMs { get; private set; }
15+
16+
[EditorBrowsable(EditorBrowsableState.Never)]
17+
[Obsolete(
18+
"Called by the de-serializer; should only be called by deriving classes for de-serialization purposes", true)]
19+
public RetryTestCase() { }
20+
21+
public RetryTestCase(
22+
IMessageSink diagnosticMessageSink,
23+
TestMethodDisplay defaultMethodDisplay,
24+
TestMethodDisplayOptions defaultMethodDisplayOptions,
25+
ITestMethod testMethod,
26+
int maxRetries,
27+
int delayBetweenRetriesMs,
28+
object[]? testMethodArguments = null)
29+
: base(diagnosticMessageSink, defaultMethodDisplay, defaultMethodDisplayOptions, testMethod,
30+
testMethodArguments)
31+
{
32+
MaxRetries = maxRetries;
33+
DelayBetweenRetriesMs = delayBetweenRetriesMs;
34+
}
35+
36+
public override Task<RunSummary> RunAsync(IMessageSink diagnosticMessageSink, IMessageBus messageBus,
37+
object[] constructorArguments, ExceptionAggregator aggregator,
38+
CancellationTokenSource cancellationTokenSource) =>
39+
RetryTestCaseRunner.RunAsync(this, diagnosticMessageSink, messageBus, cancellationTokenSource,
40+
blockingMessageBus => new XunitTestCaseRunner(this, DisplayName, SkipReason, constructorArguments,
41+
TestMethodArguments, blockingMessageBus, aggregator, cancellationTokenSource)
42+
.RunAsync());
43+
44+
public override void Serialize(IXunitSerializationInfo data)
45+
{
46+
base.Serialize(data);
47+
48+
data.AddValue("MaxRetries", MaxRetries);
49+
data.AddValue("DelayBetweenRetriesMs", DelayBetweenRetriesMs);
50+
}
51+
52+
public override void Deserialize(IXunitSerializationInfo data)
53+
{
54+
base.Deserialize(data);
55+
56+
MaxRetries = data.GetValue<int>("MaxRetries");
57+
DelayBetweenRetriesMs = data.GetValue<int>("DelayBetweenRetriesMs");
58+
}
59+
}
60+
}

0 commit comments

Comments
 (0)