/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; using ProtonVPN.Common.Core.Extensions; using ProtonVPN.Common.Legacy.Threading; using ProtonVPN.Tests.Common.Breakpoints; namespace ProtonVPN.Common.Tests.Threading { [TestClass] public class CoalescingTaskQueueTest { private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(5); [TestMethod] public async Task Enqueue_ShouldSchedule_NewTask() { // Arrange const int expected = 397; CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.Join); // Act int result = await queue.Enqueue(() => expected, expected); // Assert result.Should().Be(expected); } [TestMethod] public async Task Enqueue_ShouldJoin_RunningTask() { // Arrange const int expected = 147; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.Join); async Task TestAction(int result) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); return result; } // Act Task task1 = queue.Enqueue(() => TestAction(expected), expected); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(() => TestAction(328), 328); hit1.Continue(); // Assert task1.Result.Should().Be(expected); task2.Result.Should().Be(expected); } } [TestMethod] public async Task Enqueue_ShouldQueue_PendingTask() { // Arrange const int expected1 = 291; const int expected2 = 872; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.None); async Task TestAction(int result) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); return result; } // Act Task task1 = queue.Enqueue(() => TestAction(expected1), expected1); Task task2 = queue.Enqueue(() => TestAction(expected2), expected2); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); // Assert task1.Result.Should().Be(expected1); task2.Result.Should().Be(expected2); } } [TestMethod] public async Task Enqueue_ShouldJoin_PendingTask() { // Arrange const int expected1 = 3905; const int expected2 = 4487; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => newArg > 0 ? CoalesceDecision.None : CoalesceDecision.Join); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task1 = queue.Enqueue(ct => TestAction(expected1, ct), expected1); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(ct => TestAction(expected2, ct), expected2); Task task3 = queue.Enqueue(ct => TestAction(-15, ct), -15); hit1.Continue(); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); // Assert task1.Result.Should().Be(expected1); task2.Result.Should().Be(expected2); task3.Result.Should().Be(expected2); } } [TestMethod] public async Task Enqueue_ShouldCancel_RunningTask() { // Arrange const int expected2 = 4487; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.Cancel); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task1 = queue.Enqueue(ct => TestAction(101, ct), 101); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(ct => TestAction(expected2, ct), expected2); hit1.Continue(); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); // Assert task1.IsCanceled.Should().BeTrue(); task2.Result.Should().Be(expected2); } } [TestMethod] public async Task Enqueue_ShouldCancel_PendingTask() { // Arrange const int expected1 = 657; const int expected3 = 134; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.None); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task1 = queue.Enqueue(ct => TestAction(expected1, ct), expected1); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(ct => TestAction(101, ct), 101); Task task3 = queue.Enqueue(ct => TestAction(expected3, ct), expected3); hit1.Continue(); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); // Assert task1.Result.Should().Be(expected1); task2.IsCanceled.Should().BeTrue(); task3.Result.Should().Be(expected3); } } [TestMethod] public async Task Enqueue_ShouldCancel_PendingAndRunningTasks() { // Arrange const int expected3 = 3617; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => newArg == expected3 ? CoalesceDecision.Cancel : CoalesceDecision.None); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task1 = queue.Enqueue(ct => TestAction(202, ct), 202); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(ct => TestAction(303, ct), 303); Task task3 = queue.Enqueue(ct => TestAction(expected3, ct), expected3); hit1.Continue(); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); // Assert task1.IsCanceled.Should().BeTrue(); task2.IsCanceled.Should().BeTrue(); task3.Result.Should().Be(expected3); } } [TestMethod] public async Task Enqueue_ShouldNotJoin_CancelledRunningTask() { // Arrange const int expected = 6874; using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.Join); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act _ = queue.Enqueue(ct => TestAction(1981, ct), 1981); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); queue.Cancel(); Task task2 = queue.Enqueue(ct => TestAction(expected, ct), expected); hit1.Continue(); await breakpoint.WaitForHitAndContinue().TimeoutAfter(TestTimeout); // Assert task2.Result.Should().Be(expected); } } [TestMethod] public void Cancel_ShouldSucceed_WhenNoTasksRunning() { // Arrange CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.None); // Act Action action = () => queue.Cancel(); // Assert action.Should().NotThrow(); } [TestMethod] public async Task Cancel_ShouldCancel_RunningTask() { // Arrange using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.Join); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task = queue.Enqueue(ct => TestAction(9874, ct), 9874); BreakpointHit hit = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); queue.Cancel(); hit.Continue(); await Task.WhenAny(task); // Assert task.IsCanceled.Should().BeTrue(); } } [TestMethod] public async Task Cancel_ShouldCancel_PendingAndRunningTasks() { // Arrange using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.None); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task1 = queue.Enqueue(ct => TestAction(6517, ct), 6517); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(ct => TestAction(101, ct), 101); queue.Cancel(); hit1.Continue(); await Task.WhenAny(task1); // Assert task1.IsCanceled.Should().BeTrue(); task2.IsCanceled.Should().BeTrue(); } } [TestMethod] public async Task Cancel_ShouldCancel_PendingAndRunningTasks_UnderConcurrency() { // Arrange using (Breakpoint breakpoint = new()) { CoalescingTaskQueue queue = new CoalescingTaskQueue((newArg, arg, running) => CoalesceDecision.None); async Task TestAction(int result, CancellationToken ct) { // ReSharper disable once AccessToDisposedClosure await breakpoint.Hit().WaitForContinue(); ct.ThrowIfCancellationRequested(); return result; } // Act Task task1 = queue.Enqueue(ct => TestAction(6517, ct), 6517); BreakpointHit hit1 = await breakpoint.WaitForHit().TimeoutAfter(TestTimeout); Task task2 = queue.Enqueue(ct => TestAction(101, ct), 101); Task[] cancelTasks = Enumerable.Range(1, 10) .Select(i => Task.Run(() => queue.Cancel())) .ToArray(); await Task.WhenAny(cancelTasks); hit1.Continue(); await Task.WhenAll(cancelTasks); await Task.WhenAny(task1); // Assert task1.IsCanceled.Should().BeTrue(); task2.IsCanceled.Should().BeTrue(); } } } }