Search Issue Tracker

Active

Votes

10

Found in [Package]

Issue ID

1025676

Regression

No

UnityTests do not fail when nested coroutines throws an exception

Package: Test-Framework

-

When a test yields an IEnumerator in a coroutine (Playmode UnityTest), Unity will spawn a nested coroutine. When that throws an exception we don't stop the coroutine and fail the test, but run until it times out.

Repro steps:
Create playmode UnityTest
Add a seperate method that returns an IEnumerator and throws an exception after a yield.
Call that method in the UnityTest and yield the IEnumerator.
See the test runner just run until it times out and fails.

Comments (3)

  1. bpendleton_ms

    Apr 13, 2022 16:23

    My issue was resolved with the help of Unity support. My sample code is fixed with these changes to the CoroutineRunner:

    public static IEnumerator CoroutineRunner(IEnumerator enumerator, Action<Exception> onError, CancellationToken? cancellationToken = null)
    {
    while (true)
    {
    try
    {
    cancellationToken?.ThrowIfCancellationRequested();
    bool hasNext = enumerator.MoveNext();
    if (!hasNext) break;
    }
    catch (Exception ex)
    {
    onError(ex);
    yield break;
    }

    if (enumerator.Current == null)
    {
    yield return null;
    }
    else
    {
    // Must call recursively to catch exceptions thrown by sub-coruoutines (IEnumerator returning functions).
    IEnumerator currentEnum = enumerator.Current as IEnumerator;
    if (currentEnum != null)
    {
    yield return IterateAndCallbackOnError(currentEnum, onError, cancellationToken);
    }
    // For everything else (e.g. YieldInstruction types), just yield on the enumerator.
    else
    {
    yield return enumerator.Current;
    }
    }
    }
    }

  2. bpendleton_ms

    Feb 18, 2022 00:36

    I just discovered this issue with nested IEnumerator functions. We are using the established pattern of catching exceptions in coroutines. So this issue isn't just a testrunner issue. I'm on Unity 2021.2.6f1.

    Here's a test class that shows the problem:

    using System;
    using System.Collections;
    using System.Threading;
    using UnityEngine;

    public class TestClass : MonoBehaviour
    {
    private IEnumerator SubFunc_Throws()
    {
    yield return null;

    throw new Exception("This won't be caught"); // This exception will not be caught since we're in a subfunction and stack is reset.
    }

    private IEnumerator SubFunc_NoThrows()
    {
    yield return null;
    }

    private IEnumerator PrimaryCoroutine_ThrowsBeforeSubFunc()
    {
    yield return null;

    throw new Exception("This will be caught"); // This exception will be caught since we aren't in a subfunction.

    // yield return SubFunc_Throws();
    }

    private IEnumerator PrimaryCoroutine_ThrowsAfterSubFunc()
    {
    yield return null;

    yield return SubFunc_NoThrows();

    throw new Exception("This will be caught"); // This exception will be caught since the stack is back to normal.
    }

    private IEnumerator PrimaryCoroutine_NoThrowsSubFuncThrows()
    {
    yield return null;

    yield return SubFunc_Throws();
    }

    public static Coroutine StartCoroutineWithExceptionHandling(
    MonoBehaviour monoBehaviour,
    IEnumerator enumerator,
    Action<Exception> onError,
    CancellationToken? cancellationToken = null)
    {
    return monoBehaviour.StartCoroutine(CoroutineRunner(enumerator, onError, cancellationToken));
    }

    public static IEnumerator CoroutineRunner(IEnumerator enumerator, Action<Exception> onError, CancellationToken? cancellationToken = null)
    {
    while (true)
    {
    try
    {
    cancellationToken?.ThrowIfCancellationRequested();
    bool hasNext = enumerator.MoveNext();
    if (!hasNext) break;
    }
    catch (Exception ex)
    {
    onError(ex);
    yield break;
    }
    yield return enumerator.Current;
    }
    }

    public void Awake()
    {
    StartCoroutineWithExceptionHandling(this, PrimaryCoroutine_NoThrowsSubFuncThrows(), onError: (exception) =>
    {
    Debug.LogException(exception); // Won't get called
    });

    StartCoroutineWithExceptionHandling(this, PrimaryCoroutine_ThrowsBeforeSubFunc(), onError: (exception) =>
    {
    Debug.LogException(exception); // Will get called.
    });

    StartCoroutineWithExceptionHandling(this, PrimaryCoroutine_ThrowsAfterSubFunc(), onError: (exception) =>
    {
    Debug.LogException(exception); // Will get called.
    });
    }
    }

Add comment

Log in to post comment

All about bugs

View bugs we have successfully reproduced, and vote for the bugs you want to see fixed most urgently.