diff --git a/src/Client/LanguageClientOptions.cs b/src/Client/LanguageClientOptions.cs index 84151d878..d9e00c736 100644 --- a/src/Client/LanguageClientOptions.cs +++ b/src/Client/LanguageClientOptions.cs @@ -61,6 +61,9 @@ ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHand ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Type type, JsonRpcHandlerOptions options) => AddHandler(method, type, options); + ILanguageClientRegistry IJsonRpcHandlerRegistry.AddHandlerLink(string sourceMethod, string destinationMethod) => + AddHandlerLink(sourceMethod, destinationMethod); + ILanguageClientRegistry IJsonRpcHandlerRegistry.OnJsonRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnJsonRequest(method, handler, options); diff --git a/src/Dap.Client/DebugAdapterClientOptions.cs b/src/Dap.Client/DebugAdapterClientOptions.cs index 4bc68a0d2..a30eec58e 100644 --- a/src/Dap.Client/DebugAdapterClientOptions.cs +++ b/src/Dap.Client/DebugAdapterClientOptions.cs @@ -49,6 +49,9 @@ IDebugAdapterClientRegistry IJsonRpcHandlerRegistry IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Type type, JsonRpcHandlerOptions options) => AddHandler(method, type, options); + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.AddHandlerLink(string sourceMethod, string destinationMethod) => + AddHandlerLink(sourceMethod, destinationMethod); + IDebugAdapterClientRegistry IJsonRpcHandlerRegistry.OnJsonRequest( string method, Func> handler, JsonRpcHandlerOptions options ) => OnJsonRequest(method, handler, options); diff --git a/src/Dap.Protocol/IProgressObservable.cs b/src/Dap.Protocol/IProgressObservable.cs index f2d5f1a6b..6663b8b40 100644 --- a/src/Dap.Protocol/IProgressObservable.cs +++ b/src/Dap.Protocol/IProgressObservable.cs @@ -1,9 +1,11 @@ using System; using OmniSharp.Extensions.DebugAdapter.Protocol.Events; +using OmniSharp.Extensions.DebugAdapter.Protocol.Models; namespace OmniSharp.Extensions.DebugAdapter.Protocol { public interface IProgressObservable : IObservable { + ProgressToken ProgressToken { get; } } } diff --git a/src/Dap.Protocol/Models/ProgressToken.cs b/src/Dap.Protocol/Models/ProgressToken.cs index 8a445a42d..d54ca6b95 100644 --- a/src/Dap.Protocol/Models/ProgressToken.cs +++ b/src/Dap.Protocol/Models/ProgressToken.cs @@ -69,7 +69,6 @@ public bool Equals(ProgressToken other) => String == other.String; public bool Equals(long other) => IsLong && Long == other; - public bool Equals(string other) => IsString && String == other; private string DebuggerDisplay => IsString ? String : IsLong ? Long.ToString() : ""; diff --git a/src/Dap.Server/DebugAdapterServerOptions.cs b/src/Dap.Server/DebugAdapterServerOptions.cs index 12097495c..e8e2509b1 100644 --- a/src/Dap.Server/DebugAdapterServerOptions.cs +++ b/src/Dap.Server/DebugAdapterServerOptions.cs @@ -37,6 +37,9 @@ IDebugAdapterServerRegistry IJsonRpcHandlerRegistry IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Type type, JsonRpcHandlerOptions options) => AddHandler(method, type, options); + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.AddHandlerLink(string sourceMethod, string destinationMethod) => + AddHandlerLink(sourceMethod, destinationMethod); + IDebugAdapterServerRegistry IJsonRpcHandlerRegistry.OnJsonRequest( string method, Func> handler, JsonRpcHandlerOptions options ) => OnJsonRequest(method, handler, options); diff --git a/src/JsonRpc.Testing/AggregateSettler.cs b/src/JsonRpc.Testing/AggregateSettler.cs index d351f5756..db6fccc0c 100644 --- a/src/JsonRpc.Testing/AggregateSettler.cs +++ b/src/JsonRpc.Testing/AggregateSettler.cs @@ -13,7 +13,7 @@ public class AggregateSettler : ISettler public AggregateSettler(params ISettler[] settlers) => _settlers = settlers; - public Task SettleNext() => Settle().Take(1).IgnoreElements().LastOrDefaultAsync().ToTask(); + public Task SettleNext() => Task.WhenAll(_settlers.Select(z => z.SettleNext())); public IObservable Settle() => _settlers diff --git a/src/JsonRpc.Testing/JsonRpcIntegrationServerTestBase.cs b/src/JsonRpc.Testing/JsonRpcIntegrationServerTestBase.cs index 3de612a9d..e98fca40f 100644 --- a/src/JsonRpc.Testing/JsonRpcIntegrationServerTestBase.cs +++ b/src/JsonRpc.Testing/JsonRpcIntegrationServerTestBase.cs @@ -19,10 +19,10 @@ public JsonRpcIntegrationServerTestBase(JsonRpcTestOptions testOptions) _cancellationTokenSource = new CancellationTokenSource(); if (!Debugger.IsAttached) { - _cancellationTokenSource.CancelAfter(testOptions.TestTimeout); + _cancellationTokenSource.CancelAfter(testOptions.CancellationTimeout); } - Events = ClientEvents = new Settler(TestOptions.SettleTimeSpan, TestOptions.SettleTimeout, CancellationToken); + Events = ClientEvents = new Settler(TestOptions, CancellationToken); } protected CompositeDisposable Disposable { get; } diff --git a/src/JsonRpc.Testing/JsonRpcTestBase.cs b/src/JsonRpc.Testing/JsonRpcTestBase.cs index dade253ea..0f3d3bda8 100644 --- a/src/JsonRpc.Testing/JsonRpcTestBase.cs +++ b/src/JsonRpc.Testing/JsonRpcTestBase.cs @@ -19,22 +19,22 @@ public JsonRpcTestBase(JsonRpcTestOptions testOptions) _cancellationTokenSource = new CancellationTokenSource(); if (!Debugger.IsAttached) { - _cancellationTokenSource.CancelAfter(testOptions.TestTimeout); + _cancellationTokenSource.CancelAfter(testOptions.CancellationTimeout); } - ClientEvents = new Settler(TestOptions.SettleTimeSpan, TestOptions.SettleTimeout, CancellationToken); - ServerEvents = new Settler(TestOptions.SettleTimeSpan, TestOptions.SettleTimeout, CancellationToken); + ClientEvents = new Settler(TestOptions, CancellationToken); + ServerEvents = new Settler(TestOptions, CancellationToken); Events = new AggregateSettler(ClientEvents, ServerEvents); } protected CompositeDisposable Disposable { get; } - protected ISettler ClientEvents { get; } - protected ISettler ServerEvents { get; } - protected ISettler Events { get; } - protected JsonRpcTestOptions TestOptions { get; } - protected internal CancellationToken CancellationToken => _cancellationTokenSource.Token; - protected Task SettleNext() => Events.SettleNext(); - protected IObservable Settle() => Events.Settle(); + public ISettler ClientEvents { get; } + public ISettler ServerEvents { get; } + public ISettler Events { get; } + public JsonRpcTestOptions TestOptions { get; } + public CancellationToken CancellationToken => _cancellationTokenSource.Token; + public Task SettleNext() => Events.SettleNext(); + public IObservable Settle() => Events.Settle(); public void Dispose() { diff --git a/src/JsonRpc.Testing/JsonRpcTestOptions.cs b/src/JsonRpc.Testing/JsonRpcTestOptions.cs index 3badfc304..9a299277c 100644 --- a/src/JsonRpc.Testing/JsonRpcTestOptions.cs +++ b/src/JsonRpc.Testing/JsonRpcTestOptions.cs @@ -21,9 +21,9 @@ public JsonRpcTestOptions(ILoggerFactory clientLoggerFactory, ILoggerFactory ser public ILoggerFactory ClientLoggerFactory { get; internal set; } = NullLoggerFactory.Instance; public ILoggerFactory ServerLoggerFactory { get; internal set; } = NullLoggerFactory.Instance; - public TimeSpan SettleTimeSpan { get; internal set; } = TimeSpan.FromMilliseconds(100); - public TimeSpan SettleTimeout { get; internal set; } = TimeSpan.FromMilliseconds(500); - public TimeSpan TestTimeout { get; internal set; } = TimeSpan.FromMinutes(5); + public TimeSpan WaitTime { get; internal set; } = TimeSpan.FromMilliseconds(50); + public TimeSpan Timeout { get; internal set; } = TimeSpan.FromMilliseconds(500); + public TimeSpan CancellationTimeout { get; internal set; } = TimeSpan.FromMinutes(5); public PipeOptions DefaultPipeOptions { get; internal set; } = new PipeOptions(); } } diff --git a/src/JsonRpc.Testing/JsonRpcTestOptionsExtensions.cs b/src/JsonRpc.Testing/JsonRpcTestOptionsExtensions.cs index 82fa29733..43adfcd1f 100644 --- a/src/JsonRpc.Testing/JsonRpcTestOptionsExtensions.cs +++ b/src/JsonRpc.Testing/JsonRpcTestOptionsExtensions.cs @@ -18,21 +18,21 @@ public static JsonRpcTestOptions WithClientLoggerFactory(this JsonRpcTestOptions return options; } - public static JsonRpcTestOptions WithSettleTimeSpan(this JsonRpcTestOptions options, TimeSpan settleTimeSpan) + public static JsonRpcTestOptions WithWaitTime(this JsonRpcTestOptions options, TimeSpan waitTime) { - options.SettleTimeSpan = settleTimeSpan; + options.WaitTime = waitTime; return options; } - public static JsonRpcTestOptions WithSettleTimeout(this JsonRpcTestOptions options, TimeSpan timeout) + public static JsonRpcTestOptions WithTimeout(this JsonRpcTestOptions options, TimeSpan timeout) { - options.SettleTimeout = timeout; + options.Timeout = timeout; return options; } - public static JsonRpcTestOptions WithTestTimeout(this JsonRpcTestOptions options, TimeSpan testTimeout) + public static JsonRpcTestOptions WithCancellationTimeout(this JsonRpcTestOptions options, TimeSpan cancellationTimeout) { - options.TestTimeout = testTimeout; + options.CancellationTimeout = cancellationTimeout; return options; } diff --git a/src/JsonRpc.Testing/Settler.cs b/src/JsonRpc.Testing/Settler.cs index 599316501..5b53b7ce9 100644 --- a/src/JsonRpc.Testing/Settler.cs +++ b/src/JsonRpc.Testing/Settler.cs @@ -1,6 +1,7 @@ using System; using System.Reactive; using System.Reactive.Concurrency; +using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reactive.Threading.Tasks; using System.Threading; @@ -11,21 +12,21 @@ namespace OmniSharp.Extensions.JsonRpc.Testing { public class Settler : ISettler, IRequestSettler, IDisposable { - private readonly TimeSpan _timeout; + private readonly JsonRpcTestOptions _options; private readonly CancellationToken _cancellationToken; private readonly IScheduler _scheduler; private readonly IObservable _settle; private readonly IObserver _requester; private readonly IDisposable _connectable; - private readonly IObservable _defaultValue; + private readonly IObservable _timeoutValue; - public Settler(TimeSpan waitTime, TimeSpan timeout, CancellationToken cancellationToken, IScheduler scheduler = null) + public Settler(JsonRpcTestOptions options, CancellationToken cancellationToken, IScheduler scheduler = null) { - _timeout = timeout; + _options = options; _cancellationToken = cancellationToken; scheduler ??= Scheduler.Immediate; _scheduler = scheduler; - _defaultValue = Return(Unit.Default, _scheduler); + _timeoutValue = Return(Unit.Default, _scheduler); var subject = new Subject(); var data = subject; @@ -42,24 +43,34 @@ public Settler(TimeSpan waitTime, TimeSpan timeout, CancellationToken cancellati z => { if (z > 0) { - return Timer(_timeout, _scheduler) - .Select(z => Unit.Default); + return Never(); +// return Timer(_options.Timeout, _scheduler) +// .Select(z => Unit.Default); } - return Amb(Timer(waitTime, _scheduler), Timer(_timeout, _scheduler)) + return Timer(_options.WaitTime, _scheduler) .Select(z => Unit.Default); } ) .Replay(1, _scheduler); _connectable = connectable.Connect(); _settle = connectable - .Switch(); + .Select(o => o.Timeout(_options.Timeout, _scheduler)) + .Switch(); _requester = subject.AsObserver(); } - public Task SettleNext() => _settle.Take(1).IgnoreElements().LastOrDefaultAsync().ToTask(_cancellationToken); + public Task SettleNext() => SettleNextInternal().ToTask(_cancellationToken); - public IObservable Settle() => _settle.Timeout(_timeout, _scheduler).Catch(_ => _defaultValue); + public IObservable SettleNextInternal() => _settle + .Catch(_ => _timeoutValue) + .Take(1) + .IgnoreElements() + .LastOrDefaultAsync(); + + public IObservable Settle() => _settle + .Timeout(_options.Timeout, _scheduler) + .Catch(_ => _timeoutValue); void IRequestSettler.OnStartRequest() => _requester.OnNext(1); diff --git a/src/JsonRpc/CompositeHandlersManager.cs b/src/JsonRpc/CompositeHandlersManager.cs index 2504ccd47..f4feb00bc 100644 --- a/src/JsonRpc/CompositeHandlersManager.cs +++ b/src/JsonRpc/CompositeHandlersManager.cs @@ -27,15 +27,40 @@ public IDisposable Add(string method, IJsonRpcHandler handler, JsonRpcHandlerOpt return result; } - public IDisposable Add(JsonRpcHandlerFactory factory, JsonRpcHandlerOptions options) => _parent.Add(factory, options); + public IDisposable Add(JsonRpcHandlerFactory factory, JsonRpcHandlerOptions options) + { + var result = _parent.Add(factory, options); + _compositeDisposable.Add(result); + return result; + } - public IDisposable Add(string method, JsonRpcHandlerFactory factory, JsonRpcHandlerOptions options) => _parent.Add(method, factory, options); + public IDisposable Add(string method, JsonRpcHandlerFactory factory, JsonRpcHandlerOptions options) + { + var result = _parent.Add(factory, options); + _compositeDisposable.Add(result); + return result; + } - public IDisposable Add(Type handlerType, JsonRpcHandlerOptions options) => _parent.Add(handlerType, options); + public IDisposable Add(Type handlerType, JsonRpcHandlerOptions options) + { + var result = _parent.Add(handlerType, options); + _compositeDisposable.Add(result); + return result; + } - public IDisposable Add(string method, Type handlerType, JsonRpcHandlerOptions options) => _parent.Add(method, handlerType, options); + public IDisposable Add(string method, Type handlerType, JsonRpcHandlerOptions options) + { + var result = _parent.Add(method, handlerType, options); + _compositeDisposable.Add(result); + return result; + } - public IDisposable AddLink(string sourceMethod, string destinationMethod) => _parent.AddLink(sourceMethod, destinationMethod); + public IDisposable AddLink(string sourceMethod, string destinationMethod) + { + var result = _parent.AddLink(sourceMethod,destinationMethod); + _compositeDisposable.Add(result); + return result; + } public CompositeDisposable GetDisposable() => _compositeDisposable; } diff --git a/src/JsonRpc/IJsonRpcHandlerRegistry.cs b/src/JsonRpc/IJsonRpcHandlerRegistry.cs index 5bc8b7b00..e344441c3 100644 --- a/src/JsonRpc/IJsonRpcHandlerRegistry.cs +++ b/src/JsonRpc/IJsonRpcHandlerRegistry.cs @@ -25,6 +25,7 @@ public interface IJsonRpcHandlerRegistry : IJsonRpcHandlerRegistry where T AddHandler(string method, JsonRpcHandlerOptions options = null) where TTHandler : IJsonRpcHandler; T AddHandler(Type type, JsonRpcHandlerOptions options = null); T AddHandler(string method, Type type, JsonRpcHandlerOptions options = null); + T AddHandlerLink(string sourceMethod, string destinationMethod); T OnJsonRequest(string method, Func> handler, JsonRpcHandlerOptions options = null); T OnJsonRequest(string method, Func> handler, JsonRpcHandlerOptions options = null); diff --git a/src/JsonRpc/InterimJsonRpcServerRegistry.cs b/src/JsonRpc/InterimJsonRpcServerRegistry.cs index d988f0ea0..3c1cb2355 100644 --- a/src/JsonRpc/InterimJsonRpcServerRegistry.cs +++ b/src/JsonRpc/InterimJsonRpcServerRegistry.cs @@ -57,5 +57,11 @@ public sealed override T AddHandler(string method, Type type, JsonRpcHandlerOpti _handlersManager.Add(method, type, options); return (T) (object) this; } + + public sealed override T AddHandlerLink(string sourceMethod, string destinationMethod) + { + _handlersManager.AddLink(sourceMethod, destinationMethod); + return (T) (object) this; + } } } diff --git a/src/JsonRpc/JsonRpcCommonMethodsBase.cs b/src/JsonRpc/JsonRpcCommonMethodsBase.cs index 80acb9405..dd8a992b5 100644 --- a/src/JsonRpc/JsonRpcCommonMethodsBase.cs +++ b/src/JsonRpc/JsonRpcCommonMethodsBase.cs @@ -112,6 +112,7 @@ public T OnNotification(string method, Func handler, Js public abstract T AddHandler(string method, JsonRpcHandlerOptions options = null) where THandler : IJsonRpcHandler; public abstract T AddHandler(Type type, JsonRpcHandlerOptions options = null); public abstract T AddHandler(string method, Type type, JsonRpcHandlerOptions options = null); + public abstract T AddHandlerLink(string sourceMethod, string destinationMethod); #endregion } diff --git a/src/JsonRpc/JsonRpcOptionsRegistryBase.cs b/src/JsonRpc/JsonRpcOptionsRegistryBase.cs index 6b6503a2e..6cacf8e4c 100644 --- a/src/JsonRpc/JsonRpcOptionsRegistryBase.cs +++ b/src/JsonRpc/JsonRpcOptionsRegistryBase.cs @@ -69,6 +69,12 @@ public sealed override T AddHandler(string method, Type type, JsonRpcHandlerOpti return (T) (object) this; } + public sealed override T AddHandlerLink(string sourceMethod, string destinationMethod) + { + Handlers.Add(JsonRpcHandlerDescription.Link(sourceMethod, destinationMethod)); + return (T) (object) this; + } + #endregion } } diff --git a/src/JsonRpc/OutputHandler.cs b/src/JsonRpc/OutputHandler.cs index 85698767e..fcfc26b43 100644 --- a/src/JsonRpc/OutputHandler.cs +++ b/src/JsonRpc/OutputHandler.cs @@ -55,7 +55,8 @@ ILogger logger pipeWriter, serializer, receiver, - new EventLoopScheduler(_ => new Thread(_) { IsBackground = true, Name = "OutputHandler" }), + TaskPoolScheduler.Default, + //new EventLoopScheduler(_ => new Thread(_) { IsBackground = true, Name = "OutputHandler" }), logger ) { diff --git a/src/Protocol/Client/WorkDone/LanguageClientWorkDoneManager.cs b/src/Protocol/Client/WorkDone/LanguageClientWorkDoneManager.cs index eb0dc9b01..19eee8e45 100644 --- a/src/Protocol/Client/WorkDone/LanguageClientWorkDoneManager.cs +++ b/src/Protocol/Client/WorkDone/LanguageClientWorkDoneManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Reactive.Disposables; +using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using MediatR; @@ -96,6 +97,19 @@ public void Dispose() _progressObservable.Dispose(); } - public IDisposable Subscribe(IObserver observer) => _progressObservable.Subscribe(observer); + public IDisposable Subscribe(IObserver observer) + { + return _progressObservable.Subscribe( + _ => { + observer.OnNext(_); + if (_ is WorkDoneProgressEnd) + { + observer.OnCompleted(); + } + }, + observer.OnError, + observer.OnCompleted + ); + } } } diff --git a/src/Server/LanguageServerOptions.cs b/src/Server/LanguageServerOptions.cs index e9e9db835..cbef2eff2 100644 --- a/src/Server/LanguageServerOptions.cs +++ b/src/Server/LanguageServerOptions.cs @@ -43,6 +43,9 @@ ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHand ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandler(string method, Type type, JsonRpcHandlerOptions options) => AddHandler(method, type, options); + ILanguageServerRegistry IJsonRpcHandlerRegistry.AddHandlerLink(string sourceMethod, string destinationMethod) => + AddHandlerLink(sourceMethod, destinationMethod); + ILanguageServerRegistry IJsonRpcHandlerRegistry.OnJsonRequest(string method, Func> handler, JsonRpcHandlerOptions options) => OnJsonRequest(method, handler, options); diff --git a/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs b/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs index eda469436..77786b7cb 100644 --- a/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs +++ b/test/Dap.Tests/Integration/ConnectionAndDisconnectionTests.cs @@ -15,9 +15,7 @@ namespace Dap.Tests.Integration public class ConnectionAndDisconnectionTests : DebugAdapterProtocolTestBase { public ConnectionAndDisconnectionTests(ITestOutputHelper outputHelper) : base( - new JsonRpcTestOptions() - .ConfigureForXUnit(outputHelper) - .WithTestTimeout(TimeSpan.FromSeconds(20)) + new JsonRpcTestOptions().ConfigureForXUnit(outputHelper) ) { } diff --git a/test/Dap.Tests/Integration/Fixtures/DebugAdapterProtocolFixture.cs b/test/Dap.Tests/Integration/Fixtures/DebugAdapterProtocolFixture.cs new file mode 100644 index 000000000..3f16c0d48 --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/DebugAdapterProtocolFixture.cs @@ -0,0 +1,51 @@ +using System; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using NSubstitute; +using OmniSharp.Extensions.DebugAdapter.Protocol.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Server; +using OmniSharp.Extensions.DebugAdapter.Testing; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration.Fixtures +{ + public class DebugAdapterProtocolFixture : DebugAdapterProtocolTestBase, IAsyncLifetime + where TConfigureFixture : IConfigureDebugAdapterProtocolFixture, new() + where TConfigureClient : IConfigureDebugAdapterClientOptions, new() + where TConfigureServer : IConfigureDebugAdapterServerOptions, new() + { + private readonly TestLoggerFactory _loggerFactory; + + public DebugAdapterProtocolFixture() : + base(new TConfigureFixture().Configure(new JsonRpcTestOptions(new TestLoggerFactory(null)))) + { + _loggerFactory = TestOptions.ServerLoggerFactory as TestLoggerFactory; + } + + public void Swap(ITestOutputHelper testOutputHelper) + { + _loggerFactory.Swap(testOutputHelper); + } + + public IDebugAdapterClient Client { get; private set; } + public IDebugAdapterServer Server { get; private set; } + public new CancellationToken CancellationToken => base.CancellationToken; + public new ISettler ClientEvents => base.ClientEvents; + public new ISettler ServerEvents => base.ServerEvents; + public new ISettler Events => base.Events; + public new Task SettleNext() => Events.SettleNext(); + public new IObservable Settle() => Events.Settle(); + + public async Task InitializeAsync() + { + var (client, server) = await Initialize(new TConfigureClient().Configure, new TConfigureServer().Configure); + Client = client; + Server = server; + } + + public Task DisposeAsync() => Task.CompletedTask; + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/Fixtures/DebugAdapterProtocolFixtureTest.cs b/test/Dap.Tests/Integration/Fixtures/DebugAdapterProtocolFixtureTest.cs new file mode 100644 index 000000000..aa6e86c0c --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/DebugAdapterProtocolFixtureTest.cs @@ -0,0 +1,42 @@ +using System; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.DebugAdapter.Protocol.Client; +using OmniSharp.Extensions.DebugAdapter.Protocol.Server; +using OmniSharp.Extensions.JsonRpc.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Dap.Tests.Integration.Fixtures +{ + public abstract class DebugAdapterProtocolFixtureTest + : IClassFixture> + where TConfigureFixture : IConfigureDebugAdapterProtocolFixture, new() + where TConfigureClient : IConfigureDebugAdapterClientOptions, new() + where TConfigureServer : IConfigureDebugAdapterServerOptions, new() + { + protected DebugAdapterProtocolFixture Fixture { get; } + + protected DebugAdapterProtocolFixtureTest(ITestOutputHelper testOutputHelper, DebugAdapterProtocolFixture fixture) + { + Fixture = fixture; + Client = fixture.Client; + Server = fixture.Server; + CancellationToken = fixture.CancellationToken; + ClientEvents = fixture.ClientEvents; + ServerEvents = fixture.ServerEvents; + Events = fixture.Events; + fixture.Swap(testOutputHelper); + } + + protected IDebugAdapterServer Server { get; } + protected IDebugAdapterClient Client { get; } + protected CancellationToken CancellationToken { get; } + public ISettler ClientEvents { get; } + public ISettler ServerEvents { get; } + public ISettler Events { get; } + public Task SettleNext() => Events.SettleNext(); + public IObservable Settle() => Events.Settle(); + } +} diff --git a/test/Dap.Tests/Integration/Fixtures/DefaultClient.cs b/test/Dap.Tests/Integration/Fixtures/DefaultClient.cs new file mode 100644 index 000000000..0f68a5fef --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/DefaultClient.cs @@ -0,0 +1,15 @@ +using OmniSharp.Extensions.DebugAdapter.Client; + +namespace Dap.Tests.Integration.Fixtures +{ + public sealed class DefaultClient : IConfigureDebugAdapterClientOptions + { + public DefaultClient() + { + } + + public void Configure(DebugAdapterClientOptions options) + { + } + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/Fixtures/DefaultOptions.cs b/test/Dap.Tests/Integration/Fixtures/DefaultOptions.cs new file mode 100644 index 000000000..867d9fa99 --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/DefaultOptions.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.JsonRpc.Testing; + +namespace Dap.Tests.Integration.Fixtures +{ + public sealed class DefaultOptions : IConfigureDebugAdapterProtocolFixture + { + public JsonRpcTestOptions Configure(JsonRpcTestOptions options) => options; + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/Fixtures/DefaultServer.cs b/test/Dap.Tests/Integration/Fixtures/DefaultServer.cs new file mode 100644 index 000000000..c8d7ea28d --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/DefaultServer.cs @@ -0,0 +1,15 @@ +using OmniSharp.Extensions.DebugAdapter.Server; + +namespace Dap.Tests.Integration.Fixtures +{ + public sealed class DefaultServer : IConfigureDebugAdapterServerOptions + { + public DefaultServer() + { + } + + public void Configure(DebugAdapterServerOptions options) + { + } + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterClientOptions.cs b/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterClientOptions.cs new file mode 100644 index 000000000..311bba8ce --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterClientOptions.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.DebugAdapter.Client; + +namespace Dap.Tests.Integration.Fixtures +{ + public interface IConfigureDebugAdapterClientOptions + { + void Configure(DebugAdapterClientOptions options); + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterProtocolFixture.cs b/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterProtocolFixture.cs new file mode 100644 index 000000000..a115218eb --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterProtocolFixture.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.JsonRpc.Testing; + +namespace Dap.Tests.Integration.Fixtures +{ + public interface IConfigureDebugAdapterProtocolFixture + { + JsonRpcTestOptions Configure(JsonRpcTestOptions options); + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterServerOptions.cs b/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterServerOptions.cs new file mode 100644 index 000000000..1b6c4bf92 --- /dev/null +++ b/test/Dap.Tests/Integration/Fixtures/IConfigureDebugAdapterServerOptions.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.DebugAdapter.Server; + +namespace Dap.Tests.Integration.Fixtures +{ + public interface IConfigureDebugAdapterServerOptions + { + void Configure(DebugAdapterServerOptions options); + } +} \ No newline at end of file diff --git a/test/Dap.Tests/Integration/ProgressTests.cs b/test/Dap.Tests/Integration/ProgressTests.cs index 8cff79965..398d7ac3a 100644 --- a/test/Dap.Tests/Integration/ProgressTests.cs +++ b/test/Dap.Tests/Integration/ProgressTests.cs @@ -18,10 +18,7 @@ namespace Dap.Tests.Integration public class ProgressTests : DebugAdapterProtocolTestBase { public ProgressTests(ITestOutputHelper outputHelper) : base( - new JsonRpcTestOptions() - .ConfigureForXUnit(outputHelper) - .WithSettleTimeSpan(TimeSpan.FromSeconds(1)) - .WithSettleTimeout(TimeSpan.FromSeconds(2)) + new JsonRpcTestOptions().ConfigureForXUnit(outputHelper) ) { } @@ -31,7 +28,7 @@ private class Data public string Value { get; set; } = "Value"; } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact(Skip = "Tests work locally - fail sometimes on ci :(")] public async Task Should_Support_Progress_From_Sever_To_Client() { var (client, server) = await Initialize(ConfigureClient, ConfigureServer); @@ -78,9 +75,7 @@ public async Task Should_Support_Progress_From_Sever_To_Client() } ); - workDoneObserver.OnCompleted(); - - await SettleNext(); + await Task.Delay(1000); var results = data.Select( z => z switch { @@ -93,7 +88,7 @@ public async Task Should_Support_Progress_From_Sever_To_Client() results.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact(Skip = "Tests work locally - fail sometimes on ci :(")] public async Task Should_Support_Cancelling_Progress_From_Server_To_Client_Request() { var (client, server) = await Initialize(ConfigureClient, ConfigureServer); @@ -126,7 +121,7 @@ public async Task Should_Support_Cancelling_Progress_From_Server_To_Client_Reque } ); - await SettleNext(); + await Settle().Take(3); sub.Dispose(); diff --git a/test/JsonRpc.Tests/AggregateSettlerTests.cs b/test/JsonRpc.Tests/AggregateSettlerTests.cs index dc32e69d4..4fd39b172 100644 --- a/test/JsonRpc.Tests/AggregateSettlerTests.cs +++ b/test/JsonRpc.Tests/AggregateSettlerTests.cs @@ -37,7 +37,7 @@ public AggregateSettlerTests(ITestOutputHelper testOutputHelper) public void Should_Complete_If_There_Are_No_Pending_Requests() { var testScheduler = new TestScheduler(); - var (settler, _, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(20), TimeSpan.FromTicks(100)); + var (settler, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(20), TimeSpan.FromTicks(100)); // simulate SettleNext var observer = testScheduler.Start(() => settler.Settle().Take(1), 100, 100, ReactiveTest.Disposed); @@ -54,7 +54,7 @@ public void Should_Complete_If_There_Are_No_Pending_Requests() public void Should_Timeout_If_A_Request_Takes_To_Long(SettlerType settlerType) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(200), TimeSpan.FromTicks(500)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(200), TimeSpan.FromTicks(500)); matcher.ScheduleAbsoluteStart(settlerType, 0); matcher.ScheduleAbsoluteEnd(settlerType, ReactiveTest.Disposed); @@ -73,7 +73,7 @@ public void Should_Timeout_If_A_Request_Takes_To_Long(SettlerType settlerType) public void Should_Wait_For_Request_To_Finish_And_Then_Wait(SettlerType settlerType) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); matcher.ScheduleRelativeStart(settlerType, 0); matcher.ScheduleRelativeEnd(settlerType, 300); @@ -92,7 +92,7 @@ public void Should_Wait_For_Request_To_Finish_And_Then_Wait(SettlerType settlerT public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait(SettlerType settlerTypeA, SettlerType settlerTypeB) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); matcher.ScheduleRelativeStart(settlerTypeA, 0); matcher.ScheduleRelativeEnd(settlerTypeA, 150); @@ -118,7 +118,7 @@ public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait(SettlerT public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait_On_Either_Side(SettlerType settlerTypeA, SettlerType settlerTypeB) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); matcher.ScheduleRelativeStart(settlerTypeA, 0); matcher.ScheduleRelativeEnd(settlerTypeA, 150); @@ -139,7 +139,7 @@ public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait_On_Eithe public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait(SettlerType settlerTypeA, SettlerType settlerTypeB) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); matcher.ScheduleAbsoluteStart(settlerTypeA, 0); matcher.ScheduleAbsoluteStart(settlerTypeB, 200); @@ -160,7 +160,7 @@ public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait(Settler public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait_On_Either_Side(SettlerType settlerTypeA, SettlerType settlerTypeB) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); matcher.ScheduleAbsoluteStart(settlerTypeA, 0); matcher.ScheduleAbsoluteStart(settlerTypeB, 200); @@ -185,7 +185,7 @@ public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait_On_Eith public void Should_Complete_After_Final_Request_Timeout(SettlerType settlerTypeA, SettlerType settlerTypeB, SettlerType settlerTypeC) { var testScheduler = new TestScheduler(); - var (settler, matcher, _, _) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + var (settler, matcher) = CreateSettlers(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); matcher.ScheduleAbsoluteStart(settlerTypeA, 0); matcher.ScheduleAbsoluteEnd(settlerTypeA, 200); @@ -244,7 +244,7 @@ public IDisposable ScheduleRelativeEnd(SettlerType settlerType, long dueTime) => }; } - private (ISettler settler, AggregateRequestSettlerScheduler matcher, IRequestSettler clientRequestSettler, IRequestSettler serverRequestSettler) CreateSettlers( + private (ISettler settler, AggregateRequestSettlerScheduler matcher) CreateSettlers( TestScheduler scheduler, TimeSpan waitTime, TimeSpan timeout ) { @@ -252,8 +252,7 @@ public IDisposable ScheduleRelativeEnd(SettlerType settlerType, long dueTime) => container1.RegisterMany( Reuse.Singleton, Parameters.Of - .Name(nameof(waitTime), defaultValue: waitTime) - .Name(nameof(timeout), defaultValue: timeout) + .Type(defaultValue: new JsonRpcTestOptions().WithWaitTime(waitTime).WithTimeout(timeout)) .Type(defaultValue: CancellationToken) .Type(defaultValue: scheduler) ); @@ -261,8 +260,7 @@ public IDisposable ScheduleRelativeEnd(SettlerType settlerType, long dueTime) => container2.RegisterMany( Reuse.Singleton, Parameters.Of - .Name(nameof(waitTime), defaultValue: waitTime) - .Name(nameof(timeout), defaultValue: timeout) + .Type(defaultValue: new JsonRpcTestOptions().WithWaitTime(waitTime).WithTimeout(timeout)) .Type(defaultValue: CancellationToken) .Type(defaultValue: scheduler) ); @@ -271,8 +269,7 @@ public IDisposable ScheduleRelativeEnd(SettlerType settlerType, long dueTime) => var clientSettler = container1.Resolve(); var serverSettler = container2.Resolve(); - return ( settler, new AggregateRequestSettlerScheduler(scheduler, clientSettler, serverSettler), container1.Resolve(), - container2.Resolve() ); + return ( settler, new AggregateRequestSettlerScheduler(scheduler, clientSettler, serverSettler) ); } public enum SettlerType diff --git a/test/JsonRpc.Tests/SettlerTests.cs b/test/JsonRpc.Tests/SettlerTests.cs index cebb13700..0f6c086f1 100644 --- a/test/JsonRpc.Tests/SettlerTests.cs +++ b/test/JsonRpc.Tests/SettlerTests.cs @@ -4,6 +4,7 @@ using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; +using System.Threading.Tasks; using DryIoc; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; @@ -39,7 +40,6 @@ public void Should_Complete_If_There_Are_No_Pending_Requests() var testScheduler = new TestScheduler(); var (settler, _) = CreateSettler(testScheduler, TimeSpan.FromTicks(20), TimeSpan.FromTicks(100)); - // simulate SettleNext var observer = testScheduler.Start(() => settler.Settle().Take(1), 100, 100, ReactiveTest.Disposed); observer.Messages.Should().ContainInOrder( @@ -48,6 +48,20 @@ public void Should_Complete_If_There_Are_No_Pending_Requests() ); } + [Fact] + public async Task Should_Complete_If_There_Are_No_Pending_Requests_SettleNext() + { + var testScheduler = new TestScheduler(); + var (settler, _) = CreateSettler(testScheduler, TimeSpan.FromTicks(20), TimeSpan.FromTicks(100)); + + var observer = testScheduler.Start(() => settler.SettleNextInternal(), 100, 100, ReactiveTest.Disposed); + + observer.Messages.Should().ContainInOrder( + ReactiveTest.OnNext(121, Unit.Default), + ReactiveTest.OnCompleted(121, Unit.Default) + ); + } + [Fact] public void Should_Timeout_If_A_Request_Takes_To_Long() { @@ -64,6 +78,40 @@ public void Should_Timeout_If_A_Request_Takes_To_Long() ); } + [Fact] + public void Should_Timeout_If_A_Request_Takes_To_Long_SettleNext() + { + var testScheduler = new TestScheduler(); + var (settler, requestSettler) = CreateSettler(testScheduler, TimeSpan.FromTicks(200), TimeSpan.FromTicks(800)); + + testScheduler.ScheduleAbsolute(0, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(ReactiveTest.Disposed, () => requestSettler.OnEndRequest()); + var observer = testScheduler.Start(() => settler.SettleNextInternal(), 100, 100, ReactiveTest.Disposed); + + observer.Messages.Should().ContainInOrder( + ReactiveTest.OnNext(902, Unit.Default), + ReactiveTest.OnCompleted(902, Unit.Default) + ); + } + + [Fact] + public void Should_Reset_Timeout_If_A_Request_Takes_A_Long_Time_SettleNext() + { + var testScheduler = new TestScheduler(); + var (settler, requestSettler) = CreateSettler(testScheduler, TimeSpan.FromTicks(200), TimeSpan.FromTicks(800)); + + testScheduler.ScheduleAbsolute(0, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(700, () => requestSettler.OnEndRequest()); + testScheduler.ScheduleAbsolute(600, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(1300, () => requestSettler.OnEndRequest()); + var observer = testScheduler.Start(() => settler.SettleNextInternal(), 100, 100, 1600); + + observer.Messages.Should().ContainInOrder( + ReactiveTest.OnNext(1501, Unit.Default), + ReactiveTest.OnCompleted(1501, Unit.Default) + ); + } + [Fact] public void Should_Wait_For_Request_To_Finish_And_Then_Wait() { @@ -80,6 +128,22 @@ public void Should_Wait_For_Request_To_Finish_And_Then_Wait() ); } + [Fact] + public void Should_Wait_For_Request_To_Finish_And_Then_Wait_SettleNext() + { + var testScheduler = new TestScheduler(); + var (settler, requestSettler) = CreateSettler(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + + testScheduler.ScheduleAbsolute(0, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(300, () => requestSettler.OnEndRequest()); + var observer = testScheduler.Start(() => settler.SettleNextInternal(), 100, 100, ReactiveTest.Disposed); + + observer.Messages.Should().ContainInOrder( + ReactiveTest.OnNext(401, Unit.Default), + ReactiveTest.OnCompleted(401, Unit.Default) + ); + } + [Fact] public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait() { @@ -98,6 +162,24 @@ public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait() ); } + [Fact] + public void Should_Wait_For_Subsequent_Requests_To_Finish_And_Then_Wait_SettleNext() + { + var testScheduler = new TestScheduler(); + var (settler, requestSettler) = CreateSettler(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + + testScheduler.ScheduleAbsolute(0, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(150, () => requestSettler.OnEndRequest()); + testScheduler.ScheduleAbsolute(200, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(400, () => requestSettler.OnEndRequest()); + var observer = testScheduler.Start(() => settler.SettleNextInternal(), 100, 100, ReactiveTest.Disposed); + + observer.Messages.Should().ContainInOrder( + ReactiveTest.OnNext(501, Unit.Default), + ReactiveTest.OnCompleted(501, Unit.Default) + ); + } + [Fact] public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait() { @@ -116,6 +198,24 @@ public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait() ); } + [Fact] + public void Should_Wait_For_Overlapping_Requests_To_Finish_And_Then_Wait_SettleNext() + { + var testScheduler = new TestScheduler(); + var (settler, requestSettler) = CreateSettler(testScheduler, TimeSpan.FromTicks(100), TimeSpan.FromTicks(800)); + + testScheduler.ScheduleAbsolute(0, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(200, () => requestSettler.OnStartRequest()); + testScheduler.ScheduleAbsolute(250, () => requestSettler.OnEndRequest()); + testScheduler.ScheduleAbsolute(350, () => requestSettler.OnEndRequest()); + var observer = testScheduler.Start(() => settler.SettleNextInternal(), 100, 100, ReactiveTest.Disposed); + + observer.Messages.Should().ContainInOrder( + ReactiveTest.OnNext(451, Unit.Default), + ReactiveTest.OnCompleted(451, Unit.Default) + ); + } + [Fact] public void Should_Complete_After_Final_Request_Timeout() { @@ -128,7 +228,7 @@ public void Should_Complete_After_Final_Request_Timeout() testScheduler.ScheduleAbsolute(400, () => requestSettler.OnEndRequest()); testScheduler.ScheduleAbsolute(500, () => requestSettler.OnStartRequest()); testScheduler.ScheduleAbsolute(550, () => requestSettler.OnEndRequest()); - var observer = testScheduler.Start(() => settler.Settle(), 100, 100, 2000); + var observer = testScheduler.Start(() => settler.Settle(), 100, 100, 3000); observer.Messages.Should().ContainInOrder( ReactiveTest.OnNext(301, Unit.Default), @@ -139,19 +239,18 @@ public void Should_Complete_After_Final_Request_Timeout() ); } - private (ISettler settler, IRequestSettler requestSettler) CreateSettler(TestScheduler scheduler, TimeSpan waitTime, TimeSpan timeout) + private (Settler settler, IRequestSettler requestSettler) CreateSettler(TestScheduler scheduler, TimeSpan waitTime, TimeSpan timeout) { var container = CreateContainer(_loggerFactory); container.RegisterMany( Reuse.Singleton, Parameters.Of - .Name(nameof(waitTime), defaultValue: waitTime) - .Name(nameof(timeout), defaultValue: timeout) + .Type(defaultValue: new JsonRpcTestOptions().WithWaitTime(waitTime).WithTimeout(timeout)) .Type(defaultValue: CancellationToken) .Type(defaultValue: scheduler) ); - return ( container.Resolve(), container.Resolve() ); + return ( container.Resolve(), container.Resolve() ); } private static IContainer CreateContainer(ILoggerFactory loggerFactory) diff --git a/test/JsonRpc.Tests/TestLanguageServerRegistry.cs b/test/JsonRpc.Tests/TestLanguageServerRegistry.cs index 6a5f97a72..fc2f46f76 100644 --- a/test/JsonRpc.Tests/TestLanguageServerRegistry.cs +++ b/test/JsonRpc.Tests/TestLanguageServerRegistry.cs @@ -27,6 +27,7 @@ public override IJsonRpcServerRegistry AddHandler(string method, IJsonRpcHandler public override IJsonRpcServerRegistry AddHandler(Type type, JsonRpcHandlerOptions options = null) => throw new NotImplementedException(); public override IJsonRpcServerRegistry AddHandler(string method, Type type, JsonRpcHandlerOptions options = null) => throw new NotImplementedException(); + public override IJsonRpcServerRegistry AddHandlerLink(string sourceMethod, string destinationMethod) => throw new NotImplementedException(); public override IJsonRpcServerRegistry AddHandler(string method, JsonRpcHandlerFactory handlerFunc, JsonRpcHandlerOptions options = null) { diff --git a/test/Lsp.Tests/Integration/ConnectionAndDisconnectionTests.cs b/test/Lsp.Tests/Integration/ConnectionAndDisconnectionTests.cs index 3b280d925..9239e8f13 100644 --- a/test/Lsp.Tests/Integration/ConnectionAndDisconnectionTests.cs +++ b/test/Lsp.Tests/Integration/ConnectionAndDisconnectionTests.cs @@ -15,9 +15,7 @@ namespace Lsp.Tests.Integration public class ConnectionAndDisconnectionTests : LanguageProtocolTestBase { public ConnectionAndDisconnectionTests(ITestOutputHelper outputHelper) : base( - new JsonRpcTestOptions() - .ConfigureForXUnit(outputHelper) - .WithTestTimeout(TimeSpan.FromSeconds(20)) + new JsonRpcTestOptions().ConfigureForXUnit(outputHelper) ) { } diff --git a/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs b/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs index 8a1c3eede..ea5308973 100644 --- a/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs +++ b/test/Lsp.Tests/Integration/DynamicRegistrationTests.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using FluentAssertions; +using Lsp.Tests.Integration.Fixtures; using NSubstitute; using OmniSharp.Extensions.JsonRpc.Testing; using OmniSharp.Extensions.LanguageProtocol.Testing; @@ -21,296 +25,295 @@ namespace Lsp.Tests.Integration { - public class DynamicRegistrationTests : LanguageProtocolTestBase + public static class DynamicRegistration { - public DynamicRegistrationTests(ITestOutputHelper outputHelper) : base( - new JsonRpcTestOptions() - .ConfigureForXUnit(outputHelper) - .WithSettleTimeSpan(TimeSpan.FromSeconds(1)) - .WithSettleTimeout(TimeSpan.FromSeconds(2)) - ) + public class DynamicRegistrationTests : LanguageProtocolFixtureTest { - } - - [Fact] - public async Task Should_Register_Dynamically_After_Initialization() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); - - client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); - - await SettleNext(); - - client.RegistrationManager.CurrentRegistrations.Should().Contain( - x => - x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "csharp") - ); - } - - [Fact(Skip = "Test fails periodically on CI but not locally")] - public async Task Should_Register_Dynamically_While_Server_Is_Running() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); - - client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); - - await ServerEvents.Settle(); - await ClientEvents.Settle(); - - server.Register( - x => x - .OnCompletion( - (@params, token) => Task.FromResult(new CompletionList()), - new CompletionRegistrationOptions { - DocumentSelector = DocumentSelector.ForLanguage("vb") - } - ) - ); - - await ServerEvents.Settle(); - await ClientEvents.Settle(); - - client.RegistrationManager.CurrentRegistrations.Should().Contain( - x => - x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "vb") - ); - } - - [Fact(Skip = "Test fails periodically on CI but not locally")] - public async Task Should_Register_Links_Dynamically_While_Server_Is_Running() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); - - client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); - - await SettleNext(); - - server.Register( - x => x - .OnCompletion( + [Fact] + public void Should_Register_Dynamically_After_Initialization() + { + Client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); + + Client.RegistrationManager.CurrentRegistrations.Should().Contain( + x => + x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "csharp") + ); + } + + [Fact] + public async Task Should_Register_Dynamically_While_Server_Is_Running() + { + Client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); + + using var _ = Server.Register( + x => x + .OnCompletion( + (@params, token) => Task.FromResult(new CompletionList()), + new CompletionRegistrationOptions { + DocumentSelector = DocumentSelector.ForLanguage("vb") + } + ) + ); + + await WaitForRegistrationUpdate(); + Client.RegistrationManager.CurrentRegistrations.Should().Contain( + x => + x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "vb") + ); + } + + [Fact] + public async Task Should_Register_Links_Dynamically_While_Server_Is_Running() + { + Client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); + + using var _ = Server.Register( + x => x + .OnCompletion( + (@params, token) => Task.FromResult(new CompletionList()), + new CompletionRegistrationOptions { + DocumentSelector = DocumentSelector.ForLanguage("vb") + } + ) + ); + + await WaitForRegistrationUpdate(); + Client.RegistrationManager.CurrentRegistrations.Should().Contain( + x => + x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "vb") + ); + } + + [Fact] + public async Task Should_Gather_Linked_Registrations() + { + using var _ = Server.Register(r => r.AddHandlerLink(TextDocumentNames.SemanticTokensFull, "@/" + TextDocumentNames.SemanticTokensFull)); + + await WaitForRegistrationUpdate(); + + Client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == TextDocumentNames.SemanticTokensFull); + Client.RegistrationManager.CurrentRegistrations.Should().NotContain(x => x.Method == TextDocumentNames.SemanticTokensFullDelta); + Client.RegistrationManager.CurrentRegistrations.Should().NotContain(x => x.Method == TextDocumentNames.SemanticTokensRange); + Client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == "@/" + TextDocumentNames.SemanticTokensFull); + } + + [Fact] + public async Task Should_Unregister_Dynamically_While_Server_Is_Running() + { + Client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); + + var disposable = Server.Register( + x => x.OnCompletion( (@params, token) => Task.FromResult(new CompletionList()), new CompletionRegistrationOptions { DocumentSelector = DocumentSelector.ForLanguage("vb") } ) - ); - - await SettleNext(); - - client.RegistrationManager.CurrentRegistrations.Should().Contain( - x => - x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "vb") - ); - } - - [Fact] - public async Task Should_Gather_Linked_Registrations() - { - var (client, server) = await Initialize( - ConfigureClient, - options => { - ConfigureServer(options); - options.WithLink(TextDocumentNames.SemanticTokensFull, "@/" + TextDocumentNames.SemanticTokensFull); - } - ); - - await SettleNext(); - - client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == TextDocumentNames.SemanticTokensFull); - client.RegistrationManager.CurrentRegistrations.Should().NotContain(x => x.Method == TextDocumentNames.SemanticTokensFullDelta); - client.RegistrationManager.CurrentRegistrations.Should().NotContain(x => x.Method == TextDocumentNames.SemanticTokensRange); - client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == "@/" + TextDocumentNames.SemanticTokensFull); - } - - [Fact(Skip = "Test fails periodically on CI but not locally")] - public async Task Should_Unregister_Dynamically_While_Server_Is_Running() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); - - client.ServerSettings.Capabilities.CompletionProvider.Should().BeNull(); - - await Events.SettleNext(); + ); - var disposable = server.Register( - x => x.OnCompletion( - (@params, token) => Task.FromResult(new CompletionList()), - new CompletionRegistrationOptions { - DocumentSelector = DocumentSelector.ForLanguage("vb") + var registrations = await Observable.Create>( + observer => { + disposable.Dispose(); + return Client.RegistrationManager.Registrations.Throttle(TestOptions.WaitTime).Take(1).Subscribe(observer); } - ) - ); - - await SettleNext(); - disposable.Dispose(); - await SettleNext(); - - client.RegistrationManager.CurrentRegistrations.Should().NotContain( - x => - x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "vb") - ); + ).ToTask(CancellationToken); + + registrations.Should().NotContain( + x => + x.Method == TextDocumentNames.Completion && SelectorMatches(x, z => z.HasLanguage && z.Language == "vb") + ); + } + + private bool SelectorMatches(Registration registration, Func documentFilter) => SelectorMatches(registration.RegisterOptions, documentFilter); + + private bool SelectorMatches(object options, Func documentFilter) + { + if (options is ITextDocumentRegistrationOptions tdro) + return tdro.DocumentSelector.Any(documentFilter); + if (options is DocumentSelector selector) + return selector.Any(documentFilter); + return false; + } + + private Task WaitForRegistrationUpdate() + { + return Client.RegistrationManager.Registrations + .Throttle(TestOptions.WaitTime) + .Take(1) + .ToTask(CancellationToken); + } + + public DynamicRegistrationTests(ITestOutputHelper testOutputHelper, LanguageProtocolFixture fixture) : base( + testOutputHelper, fixture + ) + { + } } - [Fact] - public async Task Should_Gather_Static_Registrations() + public class StaticDynamicRegistrationTests : LanguageProtocolTestBase { - var (client, server) = await Initialize( - ConfigureClient, - options => { - ConfigureServer(options); - var semanticRegistrationOptions = new SemanticTokensRegistrationOptions { - Id = Guid.NewGuid().ToString(), - Legend = new SemanticTokensLegend(), - Full = new SemanticTokensCapabilityRequestFull { Delta = true }, - Range = new SemanticTokensCapabilityRequestRange(), - DocumentSelector = DocumentSelector.ForLanguage("csharp") - }; - - // Our server only statically registers when it detects a server that does not support dynamic capabilities - // This forces it to do that. - options.OnInitialized( - (server, request, response, token) => { - response.Capabilities.SemanticTokensProvider = SemanticTokensOptions.Of( - semanticRegistrationOptions, - Enumerable.Empty() - ); - response.Capabilities.SemanticTokensProvider.Id = semanticRegistrationOptions.Id; - return Task.CompletedTask; - } - ); - } - ); - client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == TextDocumentNames.SemanticTokensFull); - } - - [Fact] - public async Task Should_Register_Static_When_Dynamic_Is_Disabled() - { - var (client, server) = await Initialize( - options => { - ConfigureClient(options); - options.DisableDynamicRegistration(); - }, ConfigureServer - ); - - client.ServerSettings.Capabilities.CompletionProvider.Should().BeEquivalentTo( - new CompletionOptions { - ResolveProvider = true, - TriggerCharacters = new Container("a", "b"), - AllCommitCharacters = new Container("1", "2"), - }, x => x.Excluding(z => z.WorkDoneProgress) - ); - server.ClientSettings.Capabilities.TextDocument.Completion.Value.Should().BeEquivalentTo( - new CompletionCapability { - CompletionItem = new CompletionItemCapability { - DeprecatedSupport = true, - DocumentationFormat = new[] { MarkupKind.Markdown }, - PreselectSupport = true, - SnippetSupport = true, - TagSupport = new CompletionItemTagSupportCapability { - ValueSet = new[] { - CompletionItemTag.Deprecated + public StaticDynamicRegistrationTests(ITestOutputHelper testOutputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(testOutputHelper)) + { + } + + [Fact] + public async Task Should_Gather_Static_Registrations() + { + var (client, server) = await Initialize( + new ConfigureClient().Configure, + options => { + new ConfigureServer().Configure(options); + var semanticRegistrationOptions = new SemanticTokensRegistrationOptions { + Id = Guid.NewGuid().ToString(), + Legend = new SemanticTokensLegend(), + Full = new SemanticTokensCapabilityRequestFull { Delta = true }, + Range = new SemanticTokensCapabilityRequestRange(), + DocumentSelector = DocumentSelector.ForLanguage("csharp") + }; + + // Our server only statically registers when it detects a server that does not support dynamic capabilities + // This forces it to do that. + options.OnInitialized( + (server, request, response, token) => { + response.Capabilities.SemanticTokensProvider = SemanticTokensOptions.Of( + semanticRegistrationOptions, + Enumerable.Empty() + ); + response.Capabilities.SemanticTokensProvider.Id = semanticRegistrationOptions.Id; + return Task.CompletedTask; } - }, - CommitCharactersSupport = true - }, - ContextSupport = true, - CompletionItemKind = new CompletionItemKindCapability { - ValueSet = new Container( - Enum.GetValues(typeof(CompletionItemKind)) - .Cast() - ) + ); } - }, x => x.ConfigureForSupports().Excluding(z => z.DynamicRegistration) - ); - client.ClientSettings.Capabilities.TextDocument.Completion.Value.Should().BeEquivalentTo( - new CompletionCapability { - CompletionItem = new CompletionItemCapability { - DeprecatedSupport = true, - DocumentationFormat = new[] { MarkupKind.Markdown }, - PreselectSupport = true, - SnippetSupport = true, - TagSupport = new CompletionItemTagSupportCapability { - ValueSet = new[] { - CompletionItemTag.Deprecated - } + ); + client.RegistrationManager.CurrentRegistrations.Should().Contain(x => x.Method == TextDocumentNames.SemanticTokensFull); + } + + [Fact] + public async Task Should_Register_Static_When_Dynamic_Is_Disabled() + { + var (client, server) = await Initialize( + options => { + new ConfigureClient().Configure(options); + options.DisableDynamicRegistration(); + }, new ConfigureServer().Configure + ); + + client.ServerSettings.Capabilities.CompletionProvider.Should().BeEquivalentTo( + new CompletionOptions { + ResolveProvider = true, + TriggerCharacters = new Container("a", "b"), + AllCommitCharacters = new Container("1", "2"), + }, x => x.Excluding(z => z.WorkDoneProgress) + ); + server.ClientSettings.Capabilities.TextDocument.Completion.Value.Should().BeEquivalentTo( + new CompletionCapability { + CompletionItem = new CompletionItemCapability { + DeprecatedSupport = true, + DocumentationFormat = new[] { MarkupKind.Markdown }, + PreselectSupport = true, + SnippetSupport = true, + TagSupport = new CompletionItemTagSupportCapability { + ValueSet = new[] { + CompletionItemTag.Deprecated + } + }, + CommitCharactersSupport = true }, - CommitCharactersSupport = true - }, - ContextSupport = true, - CompletionItemKind = new CompletionItemKindCapability { - ValueSet = new Container( - Enum.GetValues(typeof(CompletionItemKind)) - .Cast() - ) - } - }, x => x.ConfigureForSupports().Excluding(z => z.DynamicRegistration) - ); + ContextSupport = true, + CompletionItemKind = new CompletionItemKindCapability { + ValueSet = new Container( + Enum.GetValues(typeof(CompletionItemKind)) + .Cast() + ) + } + }, x => x.ConfigureForSupports().Excluding(z => z.DynamicRegistration) + ); + client.ClientSettings.Capabilities.TextDocument.Completion.Value.Should().BeEquivalentTo( + new CompletionCapability { + CompletionItem = new CompletionItemCapability { + DeprecatedSupport = true, + DocumentationFormat = new[] { MarkupKind.Markdown }, + PreselectSupport = true, + SnippetSupport = true, + TagSupport = new CompletionItemTagSupportCapability { + ValueSet = new[] { + CompletionItemTag.Deprecated + } + }, + CommitCharactersSupport = true + }, + ContextSupport = true, + CompletionItemKind = new CompletionItemKindCapability { + ValueSet = new Container( + Enum.GetValues(typeof(CompletionItemKind)) + .Cast() + ) + } + }, x => x.ConfigureForSupports().Excluding(z => z.DynamicRegistration) + ); - client.RegistrationManager.CurrentRegistrations.Should().NotContain(x => x.Method == TextDocumentNames.SemanticTokensFull); + client.RegistrationManager.CurrentRegistrations.Should().NotContain(x => x.Method == TextDocumentNames.SemanticTokensFull); + } } - private void ConfigureClient(LanguageClientOptions options) + + public class ConfigureClient : IConfigureLanguageClientOptions { - options.WithCapability( - new CompletionCapability { - CompletionItem = new CompletionItemCapability { - DeprecatedSupport = true, - DocumentationFormat = new[] { MarkupKind.Markdown }, - PreselectSupport = true, - SnippetSupport = true, - TagSupport = new CompletionItemTagSupportCapability { - ValueSet = new[] { - CompletionItemTag.Deprecated - } + public void Configure(LanguageClientOptions options) + { + options.WithCapability( + new CompletionCapability { + CompletionItem = new CompletionItemCapability { + DeprecatedSupport = true, + DocumentationFormat = new[] { MarkupKind.Markdown }, + PreselectSupport = true, + SnippetSupport = true, + TagSupport = new CompletionItemTagSupportCapability { + ValueSet = new[] { + CompletionItemTag.Deprecated + } + }, + CommitCharactersSupport = true }, - CommitCharactersSupport = true - }, - ContextSupport = true, - CompletionItemKind = new CompletionItemKindCapability { - ValueSet = new Container( - Enum.GetValues(typeof(CompletionItemKind)) - .Cast() - ) + ContextSupport = true, + CompletionItemKind = new CompletionItemKindCapability { + ValueSet = new Container( + Enum.GetValues(typeof(CompletionItemKind)) + .Cast() + ) + } } - } - ); + ); - options.WithCapability( - new SemanticTokensCapability { - TokenModifiers = SemanticTokenModifier.Defaults.ToArray(), - TokenTypes = SemanticTokenType.Defaults.ToArray() - } - ); + options.WithCapability( + new SemanticTokensCapability { + TokenModifiers = SemanticTokenModifier.Defaults.ToArray(), + TokenTypes = SemanticTokenType.Defaults.ToArray() + } + ); + } } - private void ConfigureServer(LanguageServerOptions options) + public class ConfigureServer : IConfigureLanguageServerOptions { - options.OnCompletion( - (@params, token) => Task.FromResult(new CompletionList()), - new CompletionRegistrationOptions { - DocumentSelector = DocumentSelector.ForLanguage("csharp"), - ResolveProvider = false, - TriggerCharacters = new Container("a", "b"), - AllCommitCharacters = new Container("1", "2"), - } - ); - - options.OnSemanticTokens( - (builder, @params, ct) => { return Task.CompletedTask; }, - (@params, token) => { return Task.FromResult(new SemanticTokensDocument(new SemanticTokensLegend())); }, - new SemanticTokensRegistrationOptions() - ); - } - - private bool SelectorMatches(Registration registration, Func documentFilter) => SelectorMatches(registration.RegisterOptions, documentFilter); - - private bool SelectorMatches(object options, Func documentFilter) - { - if (options is ITextDocumentRegistrationOptions tdro) - return tdro.DocumentSelector.Any(documentFilter); - if (options is DocumentSelector selector) - return selector.Any(documentFilter); - return false; + public void Configure(LanguageServerOptions options) + { + options.OnCompletion( + (@params, token) => Task.FromResult(new CompletionList()), + new CompletionRegistrationOptions { + DocumentSelector = DocumentSelector.ForLanguage("csharp"), + ResolveProvider = false, + TriggerCharacters = new Container("a", "b"), + AllCommitCharacters = new Container("1", "2"), + } + ); + + options.OnSemanticTokens( + (builder, @params, ct) => { return Task.CompletedTask; }, + (@params, token) => { return Task.FromResult(new SemanticTokensDocument(new SemanticTokensLegend())); }, + new SemanticTokensRegistrationOptions() + ); + } } } } diff --git a/test/Lsp.Tests/Integration/Fixtures/DefaultClient.cs b/test/Lsp.Tests/Integration/Fixtures/DefaultClient.cs new file mode 100644 index 000000000..5df60da81 --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/DefaultClient.cs @@ -0,0 +1,15 @@ +using OmniSharp.Extensions.LanguageServer.Client; + +namespace Lsp.Tests.Integration.Fixtures +{ + public sealed class DefaultClient : IConfigureLanguageClientOptions + { + public DefaultClient() + { + } + + public void Configure(LanguageClientOptions options) + { + } + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/Fixtures/DefaultOptions.cs b/test/Lsp.Tests/Integration/Fixtures/DefaultOptions.cs new file mode 100644 index 000000000..2cf45a9d4 --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/DefaultOptions.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.JsonRpc.Testing; + +namespace Lsp.Tests.Integration.Fixtures +{ + public sealed class DefaultOptions : IConfigureLanguageProtocolFixture + { + public JsonRpcTestOptions Configure(JsonRpcTestOptions options) => options; + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/Fixtures/DefaultServer.cs b/test/Lsp.Tests/Integration/Fixtures/DefaultServer.cs new file mode 100644 index 000000000..8cbcc496d --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/DefaultServer.cs @@ -0,0 +1,15 @@ +using OmniSharp.Extensions.LanguageServer.Server; + +namespace Lsp.Tests.Integration.Fixtures +{ + public sealed class DefaultServer : IConfigureLanguageServerOptions + { + public DefaultServer() + { + } + + public void Configure(LanguageServerOptions options) + { + } + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageClientOptions.cs b/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageClientOptions.cs new file mode 100644 index 000000000..9a3975c4c --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageClientOptions.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.LanguageServer.Client; + +namespace Lsp.Tests.Integration.Fixtures +{ + public interface IConfigureLanguageClientOptions + { + void Configure(LanguageClientOptions options); + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageProtocolFixture.cs b/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageProtocolFixture.cs new file mode 100644 index 000000000..d3ffe1b54 --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageProtocolFixture.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.JsonRpc.Testing; + +namespace Lsp.Tests.Integration.Fixtures +{ + public interface IConfigureLanguageProtocolFixture + { + JsonRpcTestOptions Configure(JsonRpcTestOptions options); + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageServerOptions.cs b/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageServerOptions.cs new file mode 100644 index 000000000..cdacaecca --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/IConfigureLanguageServerOptions.cs @@ -0,0 +1,9 @@ +using OmniSharp.Extensions.LanguageServer.Server; + +namespace Lsp.Tests.Integration.Fixtures +{ + public interface IConfigureLanguageServerOptions + { + void Configure(LanguageServerOptions options); + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixture.cs b/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixture.cs new file mode 100644 index 000000000..3edd43cc5 --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixture.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using NSubstitute; +using OmniSharp.Extensions.JsonRpc.Testing; +using OmniSharp.Extensions.LanguageProtocol.Testing; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using Xunit; +using Xunit.Abstractions; + +namespace Lsp.Tests.Integration.Fixtures +{ + public class LanguageProtocolFixture : LanguageProtocolTestBase, IAsyncLifetime + where TConfigureFixture : IConfigureLanguageProtocolFixture, new() + where TConfigureClient : IConfigureLanguageClientOptions, new() + where TConfigureServer : IConfigureLanguageServerOptions, new() + { + private readonly TestLoggerFactory _loggerFactory; + + public LanguageProtocolFixture() : + base(new TConfigureFixture().Configure(new JsonRpcTestOptions(new TestLoggerFactory(null)))) + { + _loggerFactory = TestOptions.ServerLoggerFactory as TestLoggerFactory; + } + + public void Swap(ITestOutputHelper testOutputHelper) + { + _loggerFactory.Swap(testOutputHelper); + } + + public ILanguageClient Client { get; private set; } + public ILanguageServer Server { get; private set; } + + public async Task InitializeAsync() + { + var (client, server) = await Initialize(new TConfigureClient().Configure, new TConfigureServer().Configure); + Client = client; + Server = server; + } + + public Task DisposeAsync() => Task.CompletedTask; + } +} diff --git a/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixtureTest.cs b/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixtureTest.cs new file mode 100644 index 000000000..a7b12cad3 --- /dev/null +++ b/test/Lsp.Tests/Integration/Fixtures/LanguageProtocolFixtureTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using OmniSharp.Extensions.JsonRpc.Testing; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; +using Xunit; +using Xunit.Abstractions; + +namespace Lsp.Tests.Integration.Fixtures +{ + public abstract class LanguageProtocolFixtureTest + : IClassFixture> + where TConfigureFixture : IConfigureLanguageProtocolFixture, new() + where TConfigureClient : IConfigureLanguageClientOptions, new() + where TConfigureServer : IConfigureLanguageServerOptions, new() + { + protected LanguageProtocolFixture Fixture { get; } + + protected LanguageProtocolFixtureTest(ITestOutputHelper testOutputHelper, LanguageProtocolFixture fixture) + { + Fixture = fixture; + Client = fixture.Client; + Server = fixture.Server; + CancellationToken = fixture.CancellationToken; + ClientEvents = fixture.ClientEvents; + ServerEvents = fixture.ServerEvents; + Events = fixture.Events; + TestOptions = fixture.TestOptions; + fixture.Swap(testOutputHelper); + } + + protected JsonRpcTestOptions TestOptions { get; } + protected ILanguageServer Server { get; } + protected ILanguageClient Client { get; } + protected CancellationToken CancellationToken { get; } + protected ISettler ClientEvents { get; } + protected ISettler ServerEvents { get; } + protected ISettler Events { get; } + protected Task SettleNext() => Events.SettleNext(); + protected IObservable Settle() => Events.Settle(); + } +} \ No newline at end of file diff --git a/test/Lsp.Tests/Integration/PartialItemTests.cs b/test/Lsp.Tests/Integration/PartialItemTests.cs index d97f70c0a..a9cc0107f 100644 --- a/test/Lsp.Tests/Integration/PartialItemTests.cs +++ b/test/Lsp.Tests/Integration/PartialItemTests.cs @@ -6,14 +6,17 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; +using Lsp.Tests.Integration.Fixtures; using NSubstitute; using OmniSharp.Extensions.JsonRpc.Testing; using OmniSharp.Extensions.LanguageProtocol.Testing; using OmniSharp.Extensions.LanguageServer.Client; +using OmniSharp.Extensions.LanguageServer.Protocol.Client; using OmniSharp.Extensions.LanguageServer.Protocol.Client.WorkDone; using OmniSharp.Extensions.LanguageServer.Protocol.Document; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using OmniSharp.Extensions.LanguageServer.Protocol.Progress; +using OmniSharp.Extensions.LanguageServer.Protocol.Server; using OmniSharp.Extensions.LanguageServer.Protocol.Server.WorkDone; using OmniSharp.Extensions.LanguageServer.Server; using Xunit; @@ -21,120 +24,125 @@ namespace Lsp.Tests.Integration { - public class PartialItemTests : LanguageProtocolTestBase + public static class PartialItemTests { - public PartialItemTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(outputHelper)) + public class Delegates : LanguageProtocolFixtureTest { - } - - [Fact] - public async Task Should_Behave_Like_A_Task() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServerWithDelegateCodeLens); - var result = await client.TextDocument.RequestCodeLens( - new CodeLensParams { - TextDocument = new TextDocumentIdentifier(@"c:\test.cs") - }, CancellationToken - ); - - result.Should().HaveCount(3); - result.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); - } + public Delegates(ITestOutputHelper testOutputHelper, LanguageProtocolFixture fixture) : base(testOutputHelper, fixture) + { + } - [Fact] - public async Task Should_Behave_Like_An_Observable() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServerWithDelegateCodeLens); + [Fact] + public async Task Should_Behave_Like_A_Task() + { + var result = await Client.TextDocument.RequestCodeLens( + new CodeLensParams { + TextDocument = new TextDocumentIdentifier(@"c:\test.cs") + }, CancellationToken + ); - var items = new List(); - await client.TextDocument.RequestCodeLens( - new CodeLensParams { - TextDocument = new TextDocumentIdentifier(@"c:\test.cs") - }, CancellationToken - ).ForEachAsync(x => items.AddRange(x)); + result.Should().HaveCount(3); + result.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); + } - items.Should().HaveCount(3); - items.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); - } + [Fact] + public async Task Should_Behave_Like_An_Observable() + { + var items = new List(); + await Client.TextDocument.RequestCodeLens( + new CodeLensParams { + TextDocument = new TextDocumentIdentifier(@"c:\test.cs") + }, CancellationToken + ).ForEachAsync(x => items.AddRange(x)); + + items.Should().HaveCount(3); + items.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); + } - [Fact] - public async Task Should_Behave_Like_An_Observable_Without_Progress_Support() - { - var (client, server) = await Initialize(ConfigureClient, ConfigureServerWithDelegateCodeLens); + [Fact] + public async Task Should_Behave_Like_An_Observable_Without_Progress_Support() + { + var response = await Client.SendRequest( + new CodeLensParams { + TextDocument = new TextDocumentIdentifier(@"c:\test.cs") + }, CancellationToken + ); - var response = await client.SendRequest( - new CodeLensParams { - TextDocument = new TextDocumentIdentifier(@"c:\test.cs") - }, CancellationToken - ); + response.Should().HaveCount(3); + response.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); + } - response.Should().HaveCount(3); - response.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3"); + public class DelegateServer : IConfigureLanguageServerOptions + { + public void Configure(LanguageServerOptions options) => + options.OnCodeLens( + (@params, observer, capability, cancellationToken) => { + observer.OnNext( + new[] { + new CodeLens { + Command = new Command { + Name = "CodeLens 1" + } + }, + } + ); + observer.OnNext( + new[] { + new CodeLens { + Command = new Command { + Name = "CodeLens 2" + } + }, + } + ); + observer.OnNext( + new[] { + new CodeLens { + Command = new Command { + Name = "CodeLens 3" + } + }, + } + ); + observer.OnCompleted(); + }, new CodeLensRegistrationOptions() + ); + } } - - [Fact] - public async Task Should_Behave_Like_An_Observable_With_WorkDone() + public class Handlers : LanguageProtocolFixtureTest { - var (client, server) = await Initialize(ConfigureClient, ConfigureServerWithClassCodeLens); - - var items = new List(); - var work = new List(); - client.TextDocument - .ObserveWorkDone( - new CodeLensParams { TextDocument = new TextDocumentIdentifier(@"c:\test.cs") }, - (client, request) => client.RequestCodeLens(request, CancellationToken), - Observer.Create(z => work.Add(z)) - ).Subscribe(x => items.AddRange(x)); + public Handlers(ITestOutputHelper testOutputHelper, LanguageProtocolFixture fixture) : base(testOutputHelper, fixture) + { + } - await Task.Delay(1000); + [Fact] + public async Task Should_Behave_Like_An_Observable_With_WorkDone() + { + var items = new List(); + var work = new List(); + Client.TextDocument + .ObserveWorkDone( + new CodeLensParams { TextDocument = new TextDocumentIdentifier(@"c:\test.cs") }, + (client, request) => client.RequestCodeLens(request, CancellationToken), + Observer.Create(z => work.Add(z)) + ).Subscribe(x => items.AddRange(x)); - var workResults = work.Select(z => z.Message); + await Task.Delay(1000); - items.Should().HaveCount(4); - items.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3", "CodeLens 4"); + var workResults = work.Select(z => z.Message); - workResults.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); - } + items.Should().HaveCount(4); + items.Select(z => z.Command.Name).Should().ContainInOrder("CodeLens 1", "CodeLens 2", "CodeLens 3", "CodeLens 4"); - private void ConfigureClient(LanguageClientOptions options) - { - } + workResults.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); + } - private void ConfigureServerWithDelegateCodeLens(LanguageServerOptions options) => - options.OnCodeLens( - (@params, observer, capability, cancellationToken) => { - observer.OnNext( - new[] { - new CodeLens { - Command = new Command { - Name = "CodeLens 1" - } - }, - } - ); - observer.OnNext( - new[] { - new CodeLens { - Command = new Command { - Name = "CodeLens 2" - } - }, - } - ); - observer.OnNext( - new[] { - new CodeLens { - Command = new Command { - Name = "CodeLens 3" - } - }, - } - ); - observer.OnCompleted(); - }, new CodeLensRegistrationOptions() - ); - private void ConfigureServerWithClassCodeLens(LanguageServerOptions options) => options.AddHandler(); + public class HandlersServer : IConfigureLanguageServerOptions + { + public void Configure(LanguageServerOptions options) => options.AddHandler(); + } + } private class InnerCodeLensHandler : CodeLensHandler { diff --git a/test/Lsp.Tests/Integration/ProgressTests.cs b/test/Lsp.Tests/Integration/ProgressTests.cs index de76c19a6..a4256a410 100644 --- a/test/Lsp.Tests/Integration/ProgressTests.cs +++ b/test/Lsp.Tests/Integration/ProgressTests.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using FluentAssertions; +using Lsp.Tests.Integration.Fixtures; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using OmniSharp.Extensions.JsonRpc.Testing; @@ -16,9 +19,9 @@ namespace Lsp.Tests.Integration { - public class ProgressTests : LanguageProtocolTestBase + public class ProgressTests : LanguageProtocolFixtureTest { - public ProgressTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(outputHelper)) + public ProgressTests(ITestOutputHelper testOutputHelper, LanguageProtocolFixture fixture) : base(testOutputHelper, fixture) { } @@ -27,16 +30,15 @@ private class Data public string Value { get; set; } = "Value"; } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Send_Progress_From_Server_To_Client() { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); var token = new ProgressToken(Guid.NewGuid().ToString()); - var data = new List(); - - var observer = client.ProgressManager.For(token, CancellationToken); - server.ProgressManager.Monitor(token, x => x.ToObject(server.Services.GetRequiredService().JsonSerializer)).Subscribe(x => data.Add(x.Value)); + using var observer = Client.ProgressManager.For(token, CancellationToken); + var workDoneObservable = Server.ProgressManager.Monitor(token, x => x.ToObject(Server.Services.GetRequiredService().JsonSerializer)); + var observable = workDoneObservable.Replay(); + using var _ = observable.Connect(); observer.OnNext( new Data { @@ -65,21 +67,23 @@ public async Task Should_Send_Progress_From_Server_To_Client() ); await Task.Delay(1000); - observer.OnCompleted(); - data.Should().ContainInOrder("1", "3", "2", "4", "5"); + workDoneObservable.Dispose(); + + var data = await observable.Select(z => z.Value).ToArray().ToTask(CancellationToken); + + data.Should().ContainInOrder(new [] {"1", "3", "2", "4", "5" }); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Send_Progress_From_Client_To_Server() { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); var token = new ProgressToken(Guid.NewGuid().ToString()); - var data = new List(); - - using var observer = server.ProgressManager.For(token, CancellationToken); - client.ProgressManager.Monitor(token, x => x.ToObject(client.Services.GetRequiredService().JsonSerializer)).Subscribe(x => data.Add(x.Value)); + using var observer = Server.ProgressManager.For(token, CancellationToken); + var workDoneObservable = Client.ProgressManager.Monitor(token, x => x.ToObject(Client.Services.GetRequiredService().JsonSerializer)); + var observable = workDoneObservable.Replay(); + using var _ = observable.Connect(); observer.OnNext( new Data { @@ -108,30 +112,31 @@ public async Task Should_Send_Progress_From_Client_To_Server() ); await Task.Delay(1000); - observer.OnCompleted(); - data.Should().ContainInOrder("1", "3", "2", "4", "5"); + workDoneObservable.Dispose(); + + var data = await observable.Select(z => z.Value).ToArray().ToTask(CancellationToken); + + data.Should().ContainInOrder(new [] {"1", "3", "2", "4", "5" }); } [Fact] - public async Task WorkDone_Should_Be_Supported() + public void WorkDone_Should_Be_Supported() { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); - server.WorkDoneManager.IsSupported.Should().BeTrue(); - client.WorkDoneManager.IsSupported.Should().BeTrue(); + Server.WorkDoneManager.IsSupported.Should().BeTrue(); + Client.WorkDoneManager.IsSupported.Should().BeTrue(); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Support_Creating_Work_Done_From_Sever_To_Client() { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); var token = new ProgressToken(Guid.NewGuid().ToString()); - var data = new List(); - using var workDoneObservable = client.WorkDoneManager.Monitor(token); - workDoneObservable.Subscribe(x => data.Add(x)); + var workDoneObservable = Client.WorkDoneManager.Monitor(token); + var observable = workDoneObservable.Replay(); + using var _ = observable.Connect(); - using var workDoneObserver = await server.WorkDoneManager.Create( + using var workDoneObserver = await Server.WorkDoneManager.Create( token, new WorkDoneProgressBegin { Cancellable = true, Message = "Begin", @@ -172,30 +177,27 @@ public async Task Should_Support_Creating_Work_Done_From_Sever_To_Client() workDoneObserver.OnCompleted(); - await Task.Delay(1000); - - var results = data.Select( + var results = await observable.Select( z => z switch { WorkDoneProgressBegin begin => begin.Message, WorkDoneProgressReport begin => begin.Message, WorkDoneProgressEnd begin => begin.Message, } - ); + ).ToArray().ToTask(CancellationToken); results.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Support_Observing_Work_Done_From_Client_To_Server_Request() { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); var token = new ProgressToken(Guid.NewGuid().ToString()); - var data = new List(); - using var workDoneObservable = client.WorkDoneManager.Monitor(token); - workDoneObservable.Subscribe(x => data.Add(x)); + var workDoneObservable = Client.WorkDoneManager.Monitor(token); + var observable = workDoneObservable.Replay(); + using var _ = observable.Connect(); - using var workDoneObserver = await server.WorkDoneManager.Create( + using var workDoneObserver = await Server.WorkDoneManager.Create( token, new WorkDoneProgressBegin { Cancellable = true, Message = "Begin", @@ -235,31 +237,28 @@ public async Task Should_Support_Observing_Work_Done_From_Client_To_Server_Reque ); workDoneObserver.OnCompleted(); - await Task.Delay(1000); - - var results = data.Select( + var results = await observable.Select( z => z switch { WorkDoneProgressBegin begin => begin.Message, WorkDoneProgressReport begin => begin.Message, WorkDoneProgressEnd begin => begin.Message, } - ); + ).ToArray().ToTask(CancellationToken); results.Should().ContainInOrder("Begin", "Report 1", "Report 2", "Report 3", "Report 4", "End"); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Support_Cancelling_Work_Done_From_Client_To_Server_Request() { - var (client, server) = await Initialize(ConfigureClient, ConfigureServer); var token = new ProgressToken(Guid.NewGuid().ToString()); - var data = new List(); - using var workDoneObservable = client.WorkDoneManager.Monitor(token); - workDoneObservable.Subscribe(x => data.Add(x)); + var workDoneObservable = Client.WorkDoneManager.Monitor(token); + var observable = workDoneObservable.Replay(); + using var _ = observable.Connect(); - using var workDoneObserver = await server.WorkDoneManager.Create( + using var workDoneObserver = await Server.WorkDoneManager.Create( token, new WorkDoneProgressBegin { Cancellable = true, Message = "Begin", @@ -284,7 +283,8 @@ public async Task Should_Support_Cancelling_Work_Done_From_Client_To_Server_Requ } ); - await SettleNext(); + await observable.Take(3).ToTask(CancellationToken); + workDoneObservable.Dispose(); workDoneObserver.OnNext( @@ -301,28 +301,17 @@ public async Task Should_Support_Cancelling_Work_Done_From_Client_To_Server_Requ } ); - await Task.Delay(1000); - workDoneObserver.OnCompleted(); - var results = data.Select( + var results = await observable.Select( z => z switch { WorkDoneProgressBegin begin => begin.Message, WorkDoneProgressReport begin => begin.Message, WorkDoneProgressEnd begin => begin.Message, } - ); + ).ToArray().ToTask(CancellationToken); results.Should().ContainInOrder("Begin", "Report 1", "Report 2"); } - - private void ConfigureClient(LanguageClientOptions options) - { - } - - private void ConfigureServer(LanguageServerOptions options) - { - // options.OnCodeLens() - } } } diff --git a/test/Lsp.Tests/Integration/RequestCancellationTests.cs b/test/Lsp.Tests/Integration/RequestCancellationTests.cs index 36fd1f66f..a7b679678 100644 --- a/test/Lsp.Tests/Integration/RequestCancellationTests.cs +++ b/test/Lsp.Tests/Integration/RequestCancellationTests.cs @@ -130,8 +130,7 @@ public async Task Can_Publish_Diagnostics_Delayed() } ); - await ServerEvents.Settle(); - await ClientEvents.Settle(); + await SettleNext(); await Task.Delay(1000); diff --git a/test/Lsp.Tests/Integration/ShowMessageTests.cs b/test/Lsp.Tests/Integration/ShowMessageTests.cs index d7929639d..ff59b23b8 100644 --- a/test/Lsp.Tests/Integration/ShowMessageTests.cs +++ b/test/Lsp.Tests/Integration/ShowMessageTests.cs @@ -17,7 +17,7 @@ namespace Lsp.Tests.Integration { public class ShowMessageTests : LanguageProtocolTestBase { - public ShowMessageTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(outputHelper).WithSettleTimeSpan(TimeSpan.FromMilliseconds(500))) + public ShowMessageTests(ITestOutputHelper outputHelper) : base(new JsonRpcTestOptions().ConfigureForXUnit(outputHelper).WithWaitTime(TimeSpan.FromMilliseconds(500))) { } diff --git a/test/Lsp.Tests/Integration/WorkspaceFolderTests.cs b/test/Lsp.Tests/Integration/WorkspaceFolderTests.cs index f9c7f9564..a00a2423c 100644 --- a/test/Lsp.Tests/Integration/WorkspaceFolderTests.cs +++ b/test/Lsp.Tests/Integration/WorkspaceFolderTests.cs @@ -23,8 +23,8 @@ public class WorkspaceFolderTests : LanguageProtocolTestBase public WorkspaceFolderTests(ITestOutputHelper outputHelper) : base( new JsonRpcTestOptions() .ConfigureForXUnit(outputHelper, LogEventLevel.Verbose) - .WithSettleTimeSpan(TimeSpan.FromSeconds(1)) - .WithSettleTimeout(TimeSpan.FromSeconds(2)) + .WithWaitTime(TimeSpan.FromSeconds(1)) + .WithTimeout(TimeSpan.FromSeconds(2)) ) { } @@ -50,7 +50,7 @@ public async Task Should_Enable_If_Supported() server.WorkspaceFolderManager.IsSupported.Should().Be(true); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Add_A_Workspace_Folder() { var (client, server) = await Initialize(ConfigureClient, ConfigureServer); @@ -60,8 +60,7 @@ public async Task Should_Add_A_Workspace_Folder() client.WorkspaceFoldersManager.Add("/abcd/", nameof(Should_Add_A_Workspace_Folder)); - await ClientEvents.SettleNext(); - await ServerEvents.SettleNext(); + SettleNext(); folders.Should().HaveCount(1); folders[0].Event.Should().Be(WorkspaceFolderEvent.Add); @@ -77,7 +76,7 @@ public async Task Should_Have_Workspace_Folder_At_Startup() folder.Name.Should().Be(nameof(Should_Have_Workspace_Folder_At_Startup)); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Remove_Workspace_Folder_by_name() { var (client, server) = await Initialize(options => { options.WithWorkspaceFolder("/abcd/", nameof(Should_Remove_Workspace_Folder_by_name)); }, ConfigureServer); @@ -90,15 +89,14 @@ public async Task Should_Remove_Workspace_Folder_by_name() client.WorkspaceFoldersManager.Remove(nameof(Should_Remove_Workspace_Folder_by_name)); - await ClientEvents.SettleNext(); - await ServerEvents.SettleNext(); + SettleNext(); folders.Should().HaveCount(1); folders[0].Event.Should().Be(WorkspaceFolderEvent.Remove); folders[0].Folder.Name.Should().Be(nameof(Should_Remove_Workspace_Folder_by_name)); } - [Fact(Skip = "Test fails periodically on CI but not locally")] + [Fact] public async Task Should_Remove_Workspace_Folder_by_uri() { var (client, server) = await Initialize(options => { options.WithWorkspaceFolder("/abcd/", nameof(Should_Remove_Workspace_Folder_by_uri)); }, ConfigureServer); @@ -111,8 +109,7 @@ public async Task Should_Remove_Workspace_Folder_by_uri() client.WorkspaceFoldersManager.Remove(DocumentUri.From("/abcd/")); - await ClientEvents.SettleNext(); - await ServerEvents.SettleNext(); + SettleNext(); folders.Should().HaveCount(1); folders[0].Event.Should().Be(WorkspaceFolderEvent.Remove); diff --git a/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs b/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs index 73323a574..d4b5344da 100644 --- a/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs +++ b/test/TestingUtils/AutoNSubstitute/TestLoggerFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Microsoft.Extensions.Logging; using Serilog; using Serilog.Events; @@ -12,17 +13,23 @@ namespace NSubstitute public class TestLoggerFactory : ILoggerFactory { private readonly SerilogLoggerProvider _loggerProvider; + private readonly InnerTestOutputHelper _testOutputHelper; public TestLoggerFactory( ITestOutputHelper testOutputHelper, string outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}", LogEventLevel logEventLevel = LogEventLevel.Debug - ) => + ) + { + _testOutputHelper = new InnerTestOutputHelper(); + _testOutputHelper.Swap(testOutputHelper); + _loggerProvider = new SerilogLoggerProvider( new LoggerConfiguration() .MinimumLevel.Is(logEventLevel) - .WriteTo.TestOutput(testOutputHelper) + .WriteTo.TestOutput(_testOutputHelper) .CreateLogger() ); + } ILogger ILoggerFactory.CreateLogger(string categoryName) => _loggerProvider.CreateLogger(categoryName); @@ -33,5 +40,23 @@ void ILoggerFactory.AddProvider(ILoggerProvider provider) void IDisposable.Dispose() { } + + public void Swap(ITestOutputHelper testOutputHelper) + { + _testOutputHelper.Swap(testOutputHelper); + } + + class InnerTestOutputHelper : ITestOutputHelper + { + private ITestOutputHelper _testOutputHelper; + public void Swap(ITestOutputHelper testOutputHelper) + { + Interlocked.Exchange(ref _testOutputHelper, testOutputHelper); + } + + public void WriteLine(string message) => _testOutputHelper?.WriteLine(message); + + public void WriteLine(string format, params object[] args) => _testOutputHelper?.WriteLine(format, args); + } } }