diff --git a/Sources/Tests/UnitTests/Core/Multithreading/MultithreadingCancel.cs b/Sources/Tests/UnitTests/Core/Multithreading/MultithreadingCancel.cs index fb8537b78..1ae4dbef7 100644 --- a/Sources/Tests/UnitTests/Core/Multithreading/MultithreadingCancel.cs +++ b/Sources/Tests/UnitTests/Core/Multithreading/MultithreadingCancel.cs @@ -9,7 +9,6 @@ using Xunit; using AngouriMath; using AngouriMath.Extensions; -using HonkSharp.Laziness; using System.Threading.Tasks; using System.Threading; using System.Linq; @@ -19,29 +18,30 @@ namespace AngouriMath.Tests.Core.Multithreading [Trait("Area", "Core")] public sealed class MultithreadingCancel { - // Thirty solves rather than ten: warm, ten were a second and a tenth on a desktop - // against a cancellation at three quarters of a second. - private static void SomeLongLastingTask() + // Solves until it is cancelled, rather than a fixed number of times. A fixed workload put two + // clocks in this test, each wrong on some machine: it had to last three seconds, which thirty + // solves no longer do on a desktop, and a cancelled task had to stop within three quarters of + // a second, which on the Ubuntu runner it sometimes did not. A solve that starts with its token + // already cancelled has to throw. If it returns instead, the task ends as RanToCompletion and + // the assertion below says so, rather than the test running forever. + // https://github.com/asc-community/AngouriMath/issues/2118 + private static void SolveUntilCancelled(CancellationToken token) { - Entity t; - for (int i = 0; i < 30; i++) - t = - "b c d e f g a e cos(x ^ 4 + 3)1 + a f c d cos(x ^ 4 + 2)2 - k d cos(x ^ 4 + 3)2 + sin(x ^ 4 + 3) + e = 0" - .Solve("x"); + while (true) + { + var cancelledBefore = token.IsCancellationRequested; + "b c d e f g a e cos(x ^ 4 + 3)1 + a f c d cos(x ^ 4 + 2)2 - k d cos(x ^ 4 + 3)2 + sin(x ^ 4 + 3) + e = 0" + .Solve("x"); + if (cancelledBefore) + return; + } } - const int ShouldLastAtLeast = 3000; + const int CancelAt = 750; - /// - /// We are going to cancel a long-lasting task. If for some reason, it is not that long lasting, - /// this will become false - /// - public static bool MakesSenseToPerformTest => makesSenseToPerformTest.GetValue( - static @this => !new TimeOutChecker().BeingCompletedForLessThan( - SomeLongLastingTask - , ShouldLastAtLeast), someReference); - private static object someReference = new(); - private static LazyPropertyA makesSenseToPerformTest; + // How long a cancelled task may take to stop. How soon it stops is the runner's business; + // that it stops is the library's, so this is a bound no runner should reach. + const int StopBudget = 30_000; [Theory] [InlineData(false, true)] @@ -72,8 +72,6 @@ private static void SomeLongLastingTask() [InlineData(true, true, true, true, true)] public async void TestThoseOnlyCancel(bool generateChild, params bool[] threadToCancel) { - Assert.True(MakesSenseToPerformTest, $"The given task completed too soon, consider lowering the constant {ShouldLastAtLeast}"); - // Each task on a thread of its own rather than on the pool: the tasks are // processor-bound, and on a runner with two cores four of them hold every pool // thread, so the timer behind CancelAfter -- a pool work item queued behind @@ -92,8 +90,8 @@ static Task OnItsOwnThread(Action action, CancellationToken token) var task = OnItsOwnThread(() => { if (generateChild) - OnItsOwnThread(SomeLongLastingTask, token).GetAwaiter().GetResult(); - SomeLongLastingTask(); + OnItsOwnThread(() => SolveUntilCancelled(token), token).GetAwaiter().GetResult(); + SolveUntilCancelled(token); }, token); var res = (cts, task); return res; @@ -106,17 +104,19 @@ static Task OnItsOwnThread(Action action, CancellationToken token) { for (int i = 0; i < threadToCancel.Length; i++) if (threadToCancel[i]) - ctss[i].CancelAfter(ShouldLastAtLeast / 4); + ctss[i].CancelAfter(CancelAt); var waitAndSwallow = new WaiterAndSwallower(5); - await waitAndSwallow.Wait(ShouldLastAtLeast / 4); + await waitAndSwallow.Wait(CancelAt); await waitAndSwallow.WaitWhile(() => tasks.Any(c => c.Status == TaskStatus.WaitingToRun)); // wait while at least one task is waiting for execution start - await waitAndSwallow.Wait(ShouldLastAtLeast / 4); // wait some more time + await Task.WhenAny(Task.WhenAll(tasks.Where((_, i) => threadToCancel[i])), Task.Delay(StopBudget)); + await waitAndSwallow.Wait(CancelAt); // and as long again, for a task cancelled that was not meant to be } catch (OperationCanceledException) { } // we are going to check their states in finally finally { for (int i = 0; i < threadToCancel.Length; i++) - Assert.True(threadToCancel[i] == tasks[i].IsCanceled, $"Task number {i}: {threadToCancel[i]}, but instead {tasks[i].IsCanceled}. Status: {tasks[i].Status}"); + Assert.True(threadToCancel[i] ? tasks[i].IsCanceled : tasks[i].Status == TaskStatus.Running, + $"Task number {i} was {(threadToCancel[i] ? "" : "not ")}cancelled, and its status is {tasks[i].Status}"); foreach (var cts in ctss) cts.Cancel(); try