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.
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
- Unsupported Orient modes can be selected and throw errors when using Strip VFX
- "Tab" key exits the name text field in the VFX Graph tab when renaming the Property
- Shader warnings are thrown when deleting blocks in the Ribbon VFX Graph
- Shadow casters are not rendered when deploying builds in specific Unity 6.0 versions
- Automatically unfocuses when focusing and DropdownField is open
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/