From 8b3c8d15c43bc919bfde777df6bf29e644da39f8 Mon Sep 17 00:00:00 2001 From: neuecc Date: Fri, 12 Mar 2021 16:06:42 +0900 Subject: [PATCH] PlayerLoopTimer unit test and fixes --- src/UniTask.NetCore/UniTask.NetCore.csproj | 1 + .../UniTask/Runtime/PlayerLoopTimer.cs | 46 +++-- .../UniTask/Runtime/PlayerLoopTimer.cs.meta | 11 ++ .../UniTask/Runtime/TimeoutController.cs | 3 +- .../Assets/Tests/PlayerLoopTimerTest.cs | 179 ++++++++++++++++++ .../Assets/Tests/PlayerLoopTimerTest.cs.meta | 11 ++ 6 files changed, 237 insertions(+), 14 deletions(-) create mode 100644 src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta create mode 100644 src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs create mode 100644 src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta diff --git a/src/UniTask.NetCore/UniTask.NetCore.csproj b/src/UniTask.NetCore/UniTask.NetCore.csproj index 80dd978..8a2f804 100644 --- a/src/UniTask.NetCore/UniTask.NetCore.csproj +++ b/src/UniTask.NetCore/UniTask.NetCore.csproj @@ -43,6 +43,7 @@ ..\UniTask\Assets\Plugins\UniTask\Runtime\EnumeratorAsyncExtensions.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\TimeoutController.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopHelper.cs; +..\UniTask\Assets\Plugins\UniTask\Runtime\PlayerLoopTimer.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Delay.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Run.cs; ..\UniTask\Assets\Plugins\UniTask\Runtime\UniTask.Bridge.cs; diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs index 110c8cf..f8a877a 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs @@ -15,7 +15,8 @@ namespace Cysharp.Threading.Tasks readonly PlayerLoopTiming playerLoopTiming; readonly bool periodic; - bool isPlaying; + bool isRunning; + bool tryStop; bool isDisposed; protected PlayerLoopTimer(bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) @@ -64,8 +65,12 @@ namespace Cysharp.Threading.Tasks if (isDisposed) throw new ObjectDisposedException(null); ResetCore(null); // init state - isPlaying = true; - PlayerLoopHelper.AddAction(playerLoopTiming, this); + if (!isRunning) + { + isRunning = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + tryStop = false; } /// @@ -76,8 +81,12 @@ namespace Cysharp.Threading.Tasks if (isDisposed) throw new ObjectDisposedException(null); ResetCore(interval); // init state - isPlaying = true; - PlayerLoopHelper.AddAction(playerLoopTiming, this); + if (!isRunning) + { + isRunning = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + tryStop = false; } /// @@ -85,7 +94,7 @@ namespace Cysharp.Threading.Tasks /// public void Stop() { - isPlaying = false; + tryStop = true; } protected abstract void ResetCore(TimeSpan? newInterval); @@ -97,9 +106,21 @@ namespace Cysharp.Threading.Tasks bool IPlayerLoopItem.MoveNext() { - if (isDisposed) return false; - if (!isPlaying) return false; - if (cancellationToken.IsCancellationRequested) return false; + if (isDisposed) + { + isRunning = false; + return false; + } + if (tryStop) + { + isRunning = false; + return false; + } + if (cancellationToken.IsCancellationRequested) + { + isRunning = false; + return false; + } if (!MoveNextCore()) { @@ -112,6 +133,7 @@ namespace Cysharp.Threading.Tasks } else { + isRunning = false; return false; } } @@ -132,7 +154,6 @@ namespace Cysharp.Threading.Tasks : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { ResetCore(interval); - this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; } protected override bool MoveNextCore() @@ -157,6 +178,7 @@ namespace Cysharp.Threading.Tasks protected override void ResetCore(TimeSpan? interval) { this.elapsed = 0.0f; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; if (interval != null) { this.interval = (float)interval.Value.TotalSeconds; @@ -174,7 +196,6 @@ namespace Cysharp.Threading.Tasks : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) { ResetCore(interval); - this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; } protected override bool MoveNextCore() @@ -198,7 +219,8 @@ namespace Cysharp.Threading.Tasks protected override void ResetCore(TimeSpan? interval) { - elapsed = 0.0f; + this.elapsed = 0.0f; + this.initialFrame = PlayerLoopHelper.IsMainThread ? Time.frameCount : -1; if (interval != null) { this.interval = (float)interval.Value.TotalSeconds; diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta new file mode 100644 index 0000000..eb2b50a --- /dev/null +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57095a17fdca7ee4380450910afc7f26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs index e38d7b7..ad95cb5 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs @@ -76,14 +76,13 @@ namespace Cysharp.Threading.Tasks timer = null; } - var useSource = (linkedSource != null) ? linkedSource : timeoutSource; var token = useSource.Token; if (timer == null) { // Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled. // (linked)token is canceled => stop timer - timer = PlayerLoopTimer.Create(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); + timer = PlayerLoopTimer.StartNew(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); } else { diff --git a/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs new file mode 100644 index 0000000..9a91613 --- /dev/null +++ b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs @@ -0,0 +1,179 @@ +using Cysharp.Threading.Tasks; +using FluentAssertions; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine.TestTools; + +namespace Cysharp.Threading.TasksTests +{ + public class PlayerLoopTimerTest + { + void Between(TimeSpan l, TimeSpan data, TimeSpan r) + { + NUnit.Framework.Assert.AreEqual(l < data, true, "{0} < {1} failed.", l, data); + NUnit.Framework.Assert.AreEqual(data < r, true, "{0} < {1} failed.", data, r); + } + + [UnityTest] + public IEnumerator StandardTicks() => UniTask.ToCoroutine(async () => + { + foreach (var delay in new[] { DelayType.DeltaTime, DelayType.Realtime, DelayType.UnscaledDeltaTime }) + { + var raisedTimeout = new UniTaskCompletionSource(); + PlayerLoopTimer.StartNew(TimeSpan.FromSeconds(1), false, delay, PlayerLoopTiming.Update, CancellationToken.None, _ => + { + raisedTimeout.TrySetResult(); + }, null); + + + var sw = Stopwatch.StartNew(); + await raisedTimeout.Task; + sw.Stop(); + + Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1)); + } + }); + + + [UnityTest] + public IEnumerator Periodic() => UniTask.ToCoroutine(async () => + { + var raisedTime = new List(); + var count = 0; + var complete = new UniTaskCompletionSource(); + + PlayerLoopTimer timer = null; + timer = PlayerLoopTimer.StartNew(TimeSpan.FromSeconds(1), true, DelayType.DeltaTime, PlayerLoopTiming.Update, CancellationToken.None, _ => + { + raisedTime.Add(DateTime.UtcNow); + count++; + if (count == 3) + { + complete.TrySetResult(); + timer.Dispose(); + } + }, null); + + var start = DateTime.UtcNow; + await complete.Task; + + Between(TimeSpan.FromSeconds(0.9), raisedTime[0] - start, TimeSpan.FromSeconds(1.1)); + Between(TimeSpan.FromSeconds(1.9), raisedTime[1] - start, TimeSpan.FromSeconds(2.1)); + Between(TimeSpan.FromSeconds(2.9), raisedTime[2] - start, TimeSpan.FromSeconds(3.1)); + }); + + [UnityTest] + public IEnumerator CancelAfterSlimTest() => UniTask.ToCoroutine(async () => + { + var cts = new CancellationTokenSource(); + var complete = new UniTaskCompletionSource(); + cts.Token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); + }); + + cts.CancelAfterSlim(TimeSpan.FromSeconds(1)); + + var sw = Stopwatch.StartNew(); + await complete.Task; + + Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1)); + }); + + [UnityTest] + public IEnumerator CancelAfterSlimCancelTest() => UniTask.ToCoroutine(async () => + { + var cts = new CancellationTokenSource(); + var complete = new UniTaskCompletionSource(); + cts.Token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); + }); + + var d = cts.CancelAfterSlim(TimeSpan.FromSeconds(1)); + + var sw = Stopwatch.StartNew(); + + await UniTask.Delay(TimeSpan.FromMilliseconds(100)); + d.Dispose(); + + await UniTask.Delay(TimeSpan.FromSeconds(2)); + + complete.Task.Status.Should().Be(UniTaskStatus.Pending); + }); + + [UnityTest] + public IEnumerator TimeoutController() => UniTask.ToCoroutine(async () => + { + var controller = new TimeoutController(); + + var token = controller.Timeout(TimeSpan.FromSeconds(1)); + + var complete = new UniTaskCompletionSource(); + token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); + }); + + var sw = Stopwatch.StartNew(); + await complete.Task; + Between(TimeSpan.FromSeconds(0.9), sw.Elapsed, TimeSpan.FromSeconds(1.1)); + + controller.IsTimeout().Should().BeTrue(); + }); + + + [UnityTest] + public IEnumerator TimeoutReuse() => UniTask.ToCoroutine(async () => + { + var controller = new TimeoutController(DelayType.DeltaTime); + + var token = controller.Timeout(TimeSpan.FromSeconds(2)); + + var complete = new UniTaskCompletionSource(); + token.RegisterWithoutCaptureExecutionContext(() => + { + complete.TrySetResult(); // reuse, used same token? + }); + + await UniTask.Delay(TimeSpan.FromMilliseconds(100)); + controller.Reset(); + + controller.IsTimeout().Should().BeFalse(); + + var sw = Stopwatch.StartNew(); + + controller.Timeout(TimeSpan.FromSeconds(5)); + + await complete.Task; + + UnityEngine.Debug.Log(UnityEngine.Time.timeScale); + Between(TimeSpan.FromSeconds(4.9), sw.Elapsed, TimeSpan.FromSeconds(5.1)); + + controller.IsTimeout().Should().BeTrue(); + }); + + [UnityTest] + public IEnumerator LinkedTokenTest() => UniTask.ToCoroutine(async () => + { + var cts = new CancellationTokenSource(); + + var controller = new TimeoutController(cts); + var token = controller.Timeout(TimeSpan.FromSeconds(2)); + + await UniTask.DelayFrame(3); + + cts.Cancel(); + + token.IsCancellationRequested.Should().BeTrue(); + + controller.Dispose(); + }); + } +} diff --git a/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta new file mode 100644 index 0000000..dbaecbd --- /dev/null +++ b/src/UniTask/Assets/Tests/PlayerLoopTimerTest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0c49de697f829f44aa8709b4d1eff3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: