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
- [Android] Stage information is not logged when Log Shader Compilation is enabled
- [Vulkan] The memory allocation increases rapidly when there are multiple (three or more) Real-Time Reflection Probes in the Scene
- [macOS] Library folder of the opened project can be deleted which leads to the crash
- “Default Scene” dropdown field contains a spelling mistake “Default Builtin”
- Editor crashes on PPtr<Mesh> after adding Text Mesh and Cloth Components to the same GameObject
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.
});
}
}
JohnPontoco
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/