Search Issue Tracker
Active
Votes
11
Found in [Package]
Issue ID
1025676
Regression
No
UnityTests do not fail when nested coroutines throws an exception
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)
Add comment
All about bugs
View bugs we have successfully reproduced, and vote for the bugs you want to see fixed most urgently.
Latest issues
- ACES Tonemapping causes banding artifacts and negative values in ColorGradingLUT when HDR is enabled and "High Dynamic Range" Grading mode is selected while Android Platform is used
- Android Player freezes when an Audio Source is playing and an incoming call is picked up and then hung up and the Audio Source is started again
- Green success icon is poorly visible in the light Unity theme
- Incorrect input into the Input Field when using Microsoft IME Japanese
- Multiplayer role "ClientAndServer" is displayed without the spaces in the "Play Mode Scenarios" window
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;
}
}
}
}
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.
});
}
}
Kleptine
Mar 09, 2021 15:24
Here's the cause (and a suggested fix) https://forum.unity.com/threads/bug-unity-test-runner-swallows-exceptions-from-child-coroutines.1037698/