Last active
July 15, 2022 15:03
-
-
Save shanecelis/d33c5f0ae2f6f8337a50 to your computer and use it in GitHub Desktop.
I was curious about how one could manually drive Unity's coroutines without necessarily putting them into Unity's scheduler.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
CoroutineTests.cs -- Shane Celis | |
I was curious about how one could manually drive Unity's coroutines | |
without necessarily putting them into Unity's scheduler. At the | |
heart of it, it's really easy to manually drive them: | |
// Get the coroutine. | |
IEnumerator ienum = MyCoroutine(); | |
// Run it until it yields. | |
ienum.MoveNext(); | |
// Get its return value; | |
Object result = ienum.Current; | |
// Exhaust it. | |
while (ienum.MoveNext()) | |
; | |
However, to fully inspect how this machinery is all setup I wrote | |
these tests. | |
This article was pretty helpful: | |
"When a routine contains a yield then the compiler starts to do | |
some magic. That coroutine looks like a normal function in your | |
class doesn't it? Well it isn't that at all. Behind the curtain | |
the compiler has constructed a dummy class which implements | |
IEnumerator, this class has its own fields, one field for every | |
local variable you have defined in your function and another to | |
reference the instruction that last executed. | |
"When that magic class's IEnumerator.MoveNext() is called your | |
code starts to run and the compiler automagically gives you the this | |
pointer for your classes instance, rather than the one for the magic | |
class - it also works out how to reference the variables in your | |
function. In other words you've built a state machine disguised as a | |
function" [1]. | |
[1]: http://unitygems.com/advanced-coroutines/ | |
*/ | |
using System; | |
using System.Collections; | |
using NUnit.Framework; | |
using UnityEngine; | |
[TestFixture] | |
internal class CoroutineTests | |
{ | |
int state = 0; | |
/* Simple coroutine that let's us inspect what state it was in and | |
varying return values. */ | |
private IEnumerator TestCoroutine() { | |
state = 1; | |
yield return 0; | |
state = 2; | |
yield return "a"; // Notice that the return type is not strictly | |
// one type. | |
state = 3; | |
yield return 2; | |
state = 4; | |
} | |
/* | |
Let's just drive this coroutine to completion and inspect its | |
values as we go. | |
*/ | |
[Test] | |
public void ManuallyDriveCoroutineTest () | |
{ | |
Assert.AreEqual(0, state); | |
IEnumerator ienum = TestCoroutine(); | |
Assert.AreEqual(0, state); // The coroutine hasn't run yet. | |
Assert.IsTrue(ienum.MoveNext()); | |
Assert.AreEqual(0, ienum.Current); | |
Assert.AreEqual(1, state); | |
Assert.IsTrue(ienum.MoveNext()); | |
// We can return whatever kind of object we like. | |
Assert.AreEqual("a", ienum.Current); | |
Assert.AreEqual(2, state); | |
Assert.IsTrue(ienum.MoveNext()); | |
Assert.AreEqual(2, ienum.Current); | |
Assert.AreEqual(3, state); | |
Assert.IsFalse(ienum.MoveNext()); | |
// The current value remains the same as from the last call. | |
Assert.AreEqual(2, ienum.Current); | |
Assert.AreEqual(4, state); | |
} | |
private IEnumerator NestedCoroutine() { | |
yield return TestCoroutine(); | |
yield return 3; | |
} | |
/* Nested coroutines can be returned but they'd need to be run | |
* manually if you're opting out of Unity's scheduler. */ | |
[Test] | |
public void TestNestedCoroutine() { | |
IEnumerator ienum = NestedCoroutine(); | |
Assert.IsTrue(ienum.MoveNext()); | |
Assert.IsInstanceOf<IEnumerator>(ienum.Current); | |
Assert.AreEqual(0, state); // ienum has not been run. | |
Assert.IsTrue(ienum.MoveNext()); | |
Assert.AreEqual(3, ienum.Current); | |
Assert.IsFalse(ienum.MoveNext()); | |
Assert.AreEqual(3, ienum.Current); | |
} | |
/* This coroutine will pass thru another coroutine it calls. */ | |
private IEnumerator PassThruCoroutine(bool passThru) { | |
IEnumerator ienum = TestCoroutine(); | |
if (passThru) { | |
while (ienum.MoveNext()) { | |
yield return ienum.Current; | |
} | |
} else { | |
yield return ienum; | |
} | |
yield return 4; | |
} | |
/* This coroutine will manually run a nested coroutine so that it | |
all looks like just one big coroutine to the caller. */ | |
[Test] | |
public void TestPassThruCoroutine() { | |
IEnumerator ienum = PassThruCoroutine(true); | |
int count = 0; | |
while (ienum.MoveNext()) { | |
count++; | |
} | |
Assert.AreEqual(4, count); | |
ienum = PassThruCoroutine(false); | |
count = 0; | |
while (ienum.MoveNext()) { | |
count++; | |
} | |
Assert.AreEqual(2, count); | |
} | |
/* We can't reset coroutines, which is just as well. */ | |
[Test] | |
[ExpectedException (typeof (NotSupportedException))] | |
public void CoroutinesCantReset() { | |
IEnumerator ienum = TestCoroutine(); | |
ienum.Reset(); | |
} | |
[SetUp] | |
public void SetUp() { | |
state = 0; | |
} | |
[TearDown] | |
public void TearDown() { | |
state = 0; | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Genius - thanks for sharing this - I have been trying to figure out how to test coroutines, this looks like the ticket.