Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 28 additions & 28 deletions Sources/Tests/UnitTests/Core/Multithreading/MultithreadingCancel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using Xunit;
using AngouriMath;
using AngouriMath.Extensions;
using HonkSharp.Laziness;
using System.Threading.Tasks;
using System.Threading;
using System.Linq;
Expand All @@ -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;

/// <summary>
/// We are going to cancel a long-lasting task. If for some reason, it is not that long lasting,
/// this will become false
/// </summary>
public static bool MakesSenseToPerformTest => makesSenseToPerformTest.GetValue(
static @this => !new TimeOutChecker().BeingCompletedForLessThan(
SomeLongLastingTask
, ShouldLastAtLeast), someReference);
private static object someReference = new();
private static LazyPropertyA<bool> 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)]
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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
Expand Down
Loading