Search Issue Tracker
Active
Votes
10
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.
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
- Inconsistent behaviour when interacting with different dropdown types with pointer events on parent Visual Element
- Hidden GameObjects won't re-enable when they have call "DontDestroyOnLoad" function
- Overlay Canvas are rendered on each split-screen camera when HDR is enabled
- [Android] The Player loses focus when using UnityEngine.Handheld.StartActivityIndicator() with Facebook SDK
- Build fails with "Building Library/Bee/artifacts/MacStandalonePlayerBuildProgram/gahcy/hj9mx3z/951.0 failed with output:..." errors when Scripting Backend is set to IL2CPP
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/