Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ThrowsAsync works incorrectly in case of canceled task #831

Open
alexnnenn opened this issue Dec 24, 2021 · 1 comment
Open

ThrowsAsync works incorrectly in case of canceled task #831

alexnnenn opened this issue Dec 24, 2021 · 1 comment

Comments

@alexnnenn
Copy link

I've just tried testing call of function Foo and got different behaviour in test, depending on what variant I use:

private static async Task Foo()
{
    await Task.Yield();
    throw new OperationCanceledException();
}

private static async Task ShoudlyVariant2(Type exceptionType)
{
    try
    {
        var exception = await Should.ThrowAsync(Foo, exceptionType);
        Console.WriteLine("ShoudlyV2: " + exception.GetType());
    }
    catch(ShouldAssertException s) when (s.Message.Contains("but did not"))
    {
        Console.WriteLine("Variant 2 did not got " + exceptionType.Name);
    }
}

// manual variant
try
{
    await Foo();
}
catch (Exception x)
{
    Console.WriteLine("Manual:" + x.GetType());
}

// Shoudly variant 1
var exceptionV1 = await Should.ThrowAsync<Exception>(Foo);
Console.WriteLine("ShoudlyV1: " + exceptionV1.GetType());

// Shoudly variant 2
await ShoudlyVariant2(typeof(Exception));
await ShoudlyVariant2(typeof(TaskCanceledException));
await ShoudlyVariant2(typeof(OperationCanceledException));

The output is:

Manual: System.OperationCanceledException
ShoudlyV1: System.Threading.Tasks.TaskCanceledException
Variant 2 did not got Exception
Variant 2 did not got TaskCanceledException
Variant 2 did not got OperationCanceledException

I've expected that It will be OperationCanceledException in all variants. It seems, that ThrowAsync still works wrong in case of canceled task.

I've used latest available release version 4.0.3.

Originally posted by @alexnnenn in #560 (comment)

@eduard-dumitru
Copy link

eduard-dumitru commented Mar 29, 2023

I want to add the following example, which overlaps with @alexnnenn's example:

  • OperationCanceledException is not sealed. This allows TaskCanceledException to extend it but also offers an open-ended mechanism for attaching custom information to the transition of a Task's state from running to canceled.

  • Imagine you want to await the running of a System.Diagnostics.Process and limit this await with a CancellationToken. It can prove useful to extend OperationCanceledException and avoid replacing the Task.IsCanceled experience but rather augment it with, say, the stdout and stderr that had successfully been captured thus far:

public class WaitCanceledException : OperationCanceledException
{
     public string StdoutSoFar { get; }
     public string StderrSoFar { get; }
}
  • Considering you expose a method such as:
interface IApi
{
   Task<(int exitCode, string stdout, string stderr)> Run(string fileName, string arguments, CancellationToken ct);
}
  • The need to test the method could be the case:
public async Task Run_ShouldThrowWCE()
{
    using CancellationTokenSource cts = new(TimeSpan.FromMilliseconds(1));
    
    var act = () => sut.Run("foo", "bar", cts.Token);

    act.ShouldThrowAsync<WaitCanceledException>(); // 💥 will throw, causing the test to fail, which is surprising, at least for me
}

I don't know any straightforward way to access the underlying exception of a canceled task.
Nevertheless, the solution chosen here is not particularly friendly to niche use cases, where the user wants to distinguish between OperationCanceledException, TaskCanceledException, etc.:

// ShouldThrowTaskAsync.cs
// Should::ThrowAsync<TException>(Func<Task> actual, string? customMessage = null)

...
if (t.IsCanceled)
{
    return new TaskCanceledException(t);
}
...

Other famous assertion libraries conserve the actual exception:
image

One dirty fix would be, replacing new TaskCanceledException(t) with try { t.GetAwaiter().GetResult(); } catch (Exception ex) { return ex; }.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants