diff --git a/Coroutine.sln b/Coroutine.sln
index 1b25596..b8cfe5f 100644
--- a/Coroutine.sln
+++ b/Coroutine.sln
@@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Coroutine", "Coroutine\Coro
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{8BE6B559-927D-47A6-8253-D7D809D337AF}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoroutineTests", "CoroutineTests\CoroutineTests.csproj", "{8E110BC2-38FD-404A-B5BD-02C771B0D1D5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -18,5 +20,9 @@ Global
{8BE6B559-927D-47A6-8253-D7D809D337AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BE6B559-927D-47A6-8253-D7D809D337AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BE6B559-927D-47A6-8253-D7D809D337AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E110BC2-38FD-404A-B5BD-02C771B0D1D5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
diff --git a/CoroutineTests/CoroutineTests.csproj b/CoroutineTests/CoroutineTests.csproj
new file mode 100644
index 0000000..0214345
--- /dev/null
+++ b/CoroutineTests/CoroutineTests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ netcoreapp3.1
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CoroutineTests/EventBasedCoroutineTests.cs b/CoroutineTests/EventBasedCoroutineTests.cs
new file mode 100644
index 0000000..853b5f8
--- /dev/null
+++ b/CoroutineTests/EventBasedCoroutineTests.cs
@@ -0,0 +1,34 @@
+using Coroutine;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System.Collections.Generic;
+
+namespace CoroutineTests
+{
+ [TestClass]
+ public class EventBasedCoroutineTests
+ {
+ [TestMethod]
+ public void TestEventBasedCoroutine()
+ {
+ int counter = 0;
+ var myEvent = new Event();
+ IEnumerator OnEventTriggered()
+ {
+ counter++;
+ yield return new Wait(myEvent);
+ counter++;
+ }
+
+ var cr = CoroutineHandler.Start(OnEventTriggered());
+ Assert.AreEqual(1, counter, "instruction before yield is not executed.");
+ CoroutineHandler.RaiseEvent(myEvent);
+ Assert.AreEqual(2, counter, "instruction after yield is not executed.");
+ CoroutineHandler.RaiseEvent(myEvent);
+ Assert.AreEqual(2, counter, "instruction after yield is not executed.");
+
+ Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
+ Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
+ Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value.");
+ }
+ }
+}
diff --git a/CoroutineTests/TimeBasedCoroutineTests.cs b/CoroutineTests/TimeBasedCoroutineTests.cs
new file mode 100644
index 0000000..615c322
--- /dev/null
+++ b/CoroutineTests/TimeBasedCoroutineTests.cs
@@ -0,0 +1,369 @@
+using System.Collections.Generic;
+using System.Threading;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Coroutine;
+
+
+namespace CoroutineTests
+{
+ [TestClass]
+ public class TimeBasedCoroutineTests
+ {
+ [TestMethod]
+ public void TestTimerBasedCoroutine()
+ {
+ int counter = 0;
+ IEnumerator OnTimeTickCodeExecuted()
+ {
+ counter++;
+ yield return new Wait(0.1d);
+ counter++;
+ }
+
+ var cr = CoroutineHandler.Start(OnTimeTickCodeExecuted());
+ Assert.AreEqual(1, counter, "instruction before yield is not executed.");
+ Assert.AreEqual(string.Empty, cr.Name, "Incorrect default name found");
+ Assert.AreEqual(0, cr.Priority, "Default priority is not minimum");
+ for (int i = 0; i < 5; i++)
+ this.SimulateTime(1);
+ Assert.AreEqual(2, counter, "instruction after yield is not executed.");
+ Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
+ Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
+ Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value.");
+ }
+
+ [TestMethod]
+ public void TestCoroutineReturningWeirdYields()
+ {
+ int counter = 0;
+ IEnumerator OnTimeTickNeverReturnYield()
+ {
+ counter++; // 1
+ if (counter == 100) // condition that's expected to be false.
+ {
+ yield return new Wait(0.1d);
+ }
+
+ counter++; // 2
+ }
+
+ IEnumerator OnTimeTickYieldBreak()
+ {
+ counter++; // 3
+ yield break;
+ counter++; // still 3
+ }
+
+ var cr = new ActiveCoroutine[2];
+ cr[0] = CoroutineHandler.Start(OnTimeTickNeverReturnYield());
+ cr[1] = CoroutineHandler.Start(OnTimeTickYieldBreak());
+ for (int i = 0; i < 5; i++)
+ this.SimulateTime(1);
+
+ Assert.AreEqual(3, counter, $"Incorrect counter value.");
+ for (int i = 0; i < cr.Length; i++)
+ {
+ Assert.AreEqual(true, cr[i].IsFinished, $"Incorrect IsFinished value on index {i}.");
+ Assert.AreEqual(false, cr[i].WasCanceled, $"Incorrect IsCanceled value on index {i}");
+ Assert.AreEqual(1, cr[i].MoveNextCount, $"Incorrect MoveNextCount value on index {i}");
+ }
+ }
+
+ [TestMethod]
+ public void TestCoroutineReturningDefaultYield()
+ {
+ int counter = 0;
+ IEnumerator OnTimeTickYieldDefault()
+ {
+ counter++; // 1
+ yield return default;
+ counter++; // 2
+ }
+
+ var cr = CoroutineHandler.Start(OnTimeTickYieldDefault());
+ for (int i = 0; i < 5; i++)
+ this.SimulateTime(1);
+
+ Assert.AreEqual(2, counter, $"Incorrect counter value.");
+ Assert.AreEqual(true, cr.IsFinished, $"Incorrect IsFinished value.");
+ Assert.AreEqual(false, cr.WasCanceled, $"Incorrect IsCanceled value.");
+ Assert.AreEqual(2, cr.MoveNextCount, $"Incorrect MoveNextCount value.");
+ }
+
+ [TestMethod]
+ public void TestInfiniteCoroutineNeverFinishesUnlessCanceled()
+ {
+ int counter = 0;
+ IEnumerator OnTimerTickInfinite()
+ {
+ while (true)
+ {
+ counter++;
+ yield return new Wait(1);
+ }
+ }
+
+ void setCounterToUnreachableValue(ActiveCoroutine coroutine)
+ {
+ counter = -100;
+ }
+
+ var cr = CoroutineHandler.Start(OnTimerTickInfinite());
+ cr.OnFinished += setCounterToUnreachableValue;
+ for (int i = 0; i < 50; i++)
+ this.SimulateTime(1);
+
+ Assert.AreEqual(51, counter, $"Incorrect counter value.");
+ Assert.AreEqual(false, cr.IsFinished, $"Incorrect IsFinished value.");
+ Assert.AreEqual(false, cr.WasCanceled, $"Incorrect IsCanceled value.");
+ Assert.AreEqual(51, cr.MoveNextCount, $"Incorrect MoveNextCount value.");
+
+ cr.Cancel();
+ Assert.AreEqual(true, cr.WasCanceled, $"Incorrect IsCanceled value after canceling.");
+ Assert.AreEqual(-100, counter, $"OnFinished event not triggered when canceled.");
+ Assert.AreEqual(51, cr.MoveNextCount, $"Incorrect MoveNextCount value.");
+ Assert.AreEqual(true, cr.IsFinished, $"Incorrect IsFinished value.");
+ }
+
+ [TestMethod]
+ public void TestOnFinishedEventExecuted()
+ {
+ int counter = 0;
+ IEnumerator OnTimeTick()
+ {
+ counter++;
+ yield return new Wait(0.1d);
+ }
+
+ void setCounterToUnreachableValue(ActiveCoroutine coroutine)
+ {
+ counter = -100;
+ }
+
+ var cr = CoroutineHandler.Start(OnTimeTick());
+ cr.OnFinished += setCounterToUnreachableValue;
+ this.SimulateTime(50);
+ Assert.AreEqual(-100, counter, $"Incorrect counter value.");
+ }
+
+ [TestMethod]
+ public void TestNestedCoroutine()
+ {
+ int counterAlwaysRunning = 0;
+ IEnumerator AlwaysRunning()
+ {
+ while (true)
+ {
+ yield return new Wait(1);
+ counterAlwaysRunning++;
+ }
+ }
+
+ int counterChild = 0;
+ IEnumerator Child()
+ {
+ yield return new Wait(1);
+ counterChild++;
+ }
+
+ int counterParent = 0;
+ IEnumerator Parent()
+ {
+ yield return new Wait(1);
+ counterParent++;
+ // OnFinish I will start child.
+ }
+
+ int counterGrandParent = 0;
+ IEnumerator GrandParent()
+ {
+ yield return new Wait(1);
+ counterGrandParent++;
+
+ // Nested corotuine starting.
+ var p = CoroutineHandler.Start(Parent());
+
+ // Nested corotuine starting in OnFinished.
+ p.OnFinished += (ActiveCoroutine ac) => { CoroutineHandler.Start(Child()); };
+ }
+
+ CoroutineHandler.Start(AlwaysRunning());
+ CoroutineHandler.Start(GrandParent());
+ Assert.AreEqual(0, counterAlwaysRunning, "Always running counter is invalid at time 0.");
+ Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at time 0.");
+ Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 0.");
+ Assert.AreEqual(0, counterChild, "Child counter is invalid at time 0.");
+ this.SimulateTime(1);
+ Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at time 1.");
+ Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 1.");
+ Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 1.");
+ Assert.AreEqual(0, counterChild, "Child counter is invalid at time 1.");
+ this.SimulateTime(1);
+ Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at time 2.");
+ Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 2.");
+ Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 2.");
+ Assert.AreEqual(0, counterChild, "Child counter is invalid at time 2.");
+ this.SimulateTime(1);
+ Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at time 3.");
+ Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 3.");
+ Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 3.");
+ Assert.AreEqual(1, counterChild, "Child counter is invalid at time 3.");
+ this.SimulateTime(1);
+ Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at time 4.");
+ Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 4.");
+ Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 4.");
+ Assert.AreEqual(1, counterChild, "Child counter is invalid at time 4.");
+ }
+
+ [TestMethod]
+ public void TestPriority()
+ {
+ int counterShouldExecuteBefore0 = 0;
+ IEnumerator ShouldExecuteBefore0()
+ {
+ while (true)
+ {
+ yield return new Wait(1);
+ counterShouldExecuteBefore0++;
+ }
+ }
+
+ int counterShouldExecuteBefore1 = 0;
+ IEnumerator ShouldExecuteBefore1()
+ {
+ while (true)
+ {
+ yield return new Wait(1);
+ counterShouldExecuteBefore1++;
+ }
+ }
+
+ int counterShouldExecuteAfter = 0;
+ IEnumerator ShouldExecuteAfter()
+ {
+ while (true)
+ {
+ yield return new Wait(1);
+ if (counterShouldExecuteBefore0 == 1 &&
+ counterShouldExecuteBefore1 == 1)
+ {
+ counterShouldExecuteAfter++;
+ }
+ }
+ }
+
+ int counterShouldExecuteFinally = 0;
+ IEnumerator ShouldExecuteFinally()
+ {
+ while (true)
+ {
+ yield return new Wait(1);
+ if (counterShouldExecuteAfter > 0)
+ {
+ counterShouldExecuteFinally++;
+ }
+ }
+ }
+
+ int highPriority = int.MaxValue;
+ CoroutineHandler.Start(ShouldExecuteBefore1(), priority: highPriority);
+ CoroutineHandler.Start(ShouldExecuteAfter());
+ CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority);
+ CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1);
+ this.SimulateTime(10);
+ Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid.");
+ Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid.");
+ }
+
+ [TestMethod]
+ public void TestTimeBasedCoroutineIsAccurate()
+ {
+ int counter0 = 0;
+ IEnumerator IncrementCounter0Ever10Seconds()
+ {
+ while (true)
+ {
+ yield return new Wait(10);
+ counter0++;
+ }
+ }
+
+ int counter1 = 0;
+ IEnumerator IncrementCounter1Every5Seconds()
+ {
+ while (true)
+ {
+ yield return new Wait(5);
+ counter1++;
+ }
+ }
+
+ CoroutineHandler.Start(IncrementCounter0Ever10Seconds());
+ CoroutineHandler.Start(IncrementCounter1Every5Seconds());
+ this.SimulateTime(3);
+ Assert.AreEqual(0, counter0, $"Incorrect counter0 value after 3 seconds.");
+ Assert.AreEqual(0, counter1, $"Incorrect counter1 value after 3 seconds.");
+ this.SimulateTime(3);
+ Assert.AreEqual(0, counter0, $"Incorrect counter0 value after 6 seconds.");
+ Assert.AreEqual(1, counter1, $"Incorrect counter1 value after 6 seconds.");
+
+ // it's 5 over here because IncrementCounter1Every5Seconds
+ // increments 5 seconds after last yield. not 5 seconds since start.
+ // So the when we send 3 seconds in the last SimulateTime,
+ // the 3rd second was technically ignored.
+ this.SimulateTime(5);
+ Assert.AreEqual(1, counter0, $"Incorrect counter0 value after 10 seconds.");
+ Assert.AreEqual(2, counter1, $"Incorrect counter1 value after next 5 seconds.");
+ }
+
+ [TestMethod]
+ public void InvokeLaterAndNameTest()
+ {
+ int counter = 0;
+ var cr = CoroutineHandler.InvokeLater(new Wait(10), () => {
+ counter++;
+ }, "Bird");
+
+ this.SimulateTime(5);
+ Assert.AreEqual(0, counter, $"Incorrect counter value after 5 seconds.");
+ this.SimulateTime(5);
+ Assert.AreEqual(1, counter, $"Incorrect counter value after 10 seconds.");
+ Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
+ Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
+ Assert.AreEqual(cr.MoveNextCount, 2, "Incorrect MoveNextCount value.");
+ Assert.AreEqual(cr.Name, "Bird", "Incorrect name of the coroutine.");
+ }
+
+ [TestMethod]
+ public void CoroutineStatsAre95PercentAccurate()
+ {
+ IEnumerator CoroutineTakesMax500MS()
+ {
+ Thread.Sleep(200);
+ yield return new Wait(10);
+ Thread.Sleep(500);
+ }
+
+ var cr = CoroutineHandler.Start(CoroutineTakesMax500MS());
+ for (int i = 0; i < 5; i++)
+ this.SimulateTime(50);
+
+ int expected1 = 350;
+ float errorbar1 = (5 / 100f * expected1);
+ bool gTA = cr.AverageMoveNextTime.Milliseconds > (expected1 - errorbar1); // 95% accuracy.
+ bool lTB = cr.AverageMoveNextTime.Milliseconds < (expected1 + errorbar1); // 95% accuracy.
+ Assert.IsTrue(gTA && lTB, $"Average Move Next Time {cr.AverageMoveNextTime.Milliseconds} is invalid.");
+
+ int expected2 = 500;
+ float errorbar2 = (5 / 100f * expected2);
+ bool gTC = cr.MaxMoveNextTime.Milliseconds > (expected2 - errorbar2); // 95% accuracy.
+ bool lTD = cr.MaxMoveNextTime.Milliseconds < (expected2 + errorbar2); // 95% accuracy.
+ Assert.IsTrue(gTC && lTD, $"Maximum Move Next Time {cr.MaxMoveNextTime.Milliseconds} is invalid.");
+ }
+
+ private void SimulateTime(double totalSeconds)
+ {
+ CoroutineHandler.Tick(totalSeconds);
+ }
+ }
+}