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/CancellationTokenSourceExtensions.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs index 87dcbe7..c519944 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/CancellationTokenSourceExtensions.cs @@ -4,32 +4,29 @@ using System.Threading; using UnityEngine; using Cysharp.Threading.Tasks.Triggers; using System; +using Cysharp.Threading.Tasks.Internal; namespace Cysharp.Threading.Tasks { - public static class CancellationTokenSourceExtensions + public static partial class CancellationTokenSourceExtensions { - public static void CancelAfterSlim(this CancellationTokenSource cts, int millisecondsDelay, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) + readonly static Action CancelCancellationTokenSourceStateDelegate = new Action(CancelCancellationTokenSourceState); + + static void CancelCancellationTokenSourceState(object state) { - var delay = UniTask.Delay(millisecondsDelay, delayType, delayTiming, cts.Token); - CancelAfterCore(cts, delay).Forget(); + var cts = (CancellationTokenSource)state; + cts.Cancel(); } - public static void CancelAfterSlim(this CancellationTokenSource cts, TimeSpan delayTimeSpan, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) + public static IDisposable CancelAfterSlim(this CancellationTokenSource cts, int millisecondsDelay, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { - var delay = UniTask.Delay(delayTimeSpan, delayType, delayTiming, cts.Token); - CancelAfterCore(cts, delay).Forget(); + return CancelAfterSlim(cts, TimeSpan.FromMilliseconds(millisecondsDelay), delayType, delayTiming); } - static async UniTaskVoid CancelAfterCore(CancellationTokenSource cts, UniTask delayTask) + public static IDisposable CancelAfterSlim(this CancellationTokenSource cts, TimeSpan delayTimeSpan, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { - var alreadyCanceled = await delayTask.SuppressCancellationThrow(); - if (!alreadyCanceled) - { - cts.Cancel(); - cts.Dispose(); - } + return PlayerLoopTimer.StartNew(delayTimeSpan, false, delayType, delayTiming, cts.Token, CancelCancellationTokenSourceStateDelegate, cts); } public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, Component component) @@ -40,11 +37,7 @@ namespace Cysharp.Threading.Tasks public static void RegisterRaiseCancelOnDestroy(this CancellationTokenSource cts, GameObject gameObject) { var trigger = gameObject.GetAsyncDestroyTrigger(); - trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(state => - { - var cts2 = (CancellationTokenSource)state; - cts2.Cancel(); - }, cts); + trigger.CancellationToken.RegisterWithoutCaptureExecutionContext(CancelCancellationTokenSourceStateDelegate, cts); } } } diff --git a/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs new file mode 100644 index 0000000..f8a877a --- /dev/null +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/PlayerLoopTimer.cs @@ -0,0 +1,262 @@ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +using System.Threading; +using System; +using Cysharp.Threading.Tasks.Internal; +using UnityEngine; + +namespace Cysharp.Threading.Tasks +{ + public abstract class PlayerLoopTimer : IDisposable, IPlayerLoopItem + { + readonly CancellationToken cancellationToken; + readonly Action timerCallback; + readonly object state; + readonly PlayerLoopTiming playerLoopTiming; + readonly bool periodic; + + bool isRunning; + bool tryStop; + bool isDisposed; + + protected PlayerLoopTimer(bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + this.periodic = periodic; + this.playerLoopTiming = playerLoopTiming; + this.cancellationToken = cancellationToken; + this.timerCallback = timerCallback; + this.state = state; + } + + public static PlayerLoopTimer Create(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { +#if UNITY_EDITOR + // force use Realtime. + if (PlayerLoopHelper.IsMainThread && !UnityEditor.EditorApplication.isPlaying) + { + delayType = DelayType.Realtime; + } +#endif + + switch (delayType) + { + case DelayType.UnscaledDeltaTime: + return new IgnoreTimeScalePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + case DelayType.Realtime: + return new RealtimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + case DelayType.DeltaTime: + default: + return new DeltaTimePlayerLoopTimer(interval, periodic, playerLoopTiming, cancellationToken, timerCallback, state); + } + } + + public static PlayerLoopTimer StartNew(TimeSpan interval, bool periodic, DelayType delayType, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + { + var timer = Create(interval, periodic, delayType, playerLoopTiming, cancellationToken, timerCallback, state); + timer.Restart(); + return timer; + } + + /// + /// Restart(Reset and Start) timer. + /// + public void Restart() + { + if (isDisposed) throw new ObjectDisposedException(null); + + ResetCore(null); // init state + if (!isRunning) + { + isRunning = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + tryStop = false; + } + + /// + /// Restart(Reset and Start) and change interval. + /// + public void Restart(TimeSpan interval) + { + if (isDisposed) throw new ObjectDisposedException(null); + + ResetCore(interval); // init state + if (!isRunning) + { + isRunning = true; + PlayerLoopHelper.AddAction(playerLoopTiming, this); + } + tryStop = false; + } + + /// + /// Stop timer. + /// + public void Stop() + { + tryStop = true; + } + + protected abstract void ResetCore(TimeSpan? newInterval); + + public void Dispose() + { + isDisposed = true; + } + + bool IPlayerLoopItem.MoveNext() + { + if (isDisposed) + { + isRunning = false; + return false; + } + if (tryStop) + { + isRunning = false; + return false; + } + if (cancellationToken.IsCancellationRequested) + { + isRunning = false; + return false; + } + + if (!MoveNextCore()) + { + timerCallback(state); + + if (periodic) + { + ResetCore(null); + return true; + } + else + { + isRunning = false; + return false; + } + } + + return true; + } + + protected abstract bool MoveNextCore(); + } + + sealed class DeltaTimePlayerLoopTimer : PlayerLoopTimer + { + int initialFrame; + float elapsed; + float interval; + + public DeltaTimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) + { + ResetCore(interval); + } + + protected override bool MoveNextCore() + { + if (elapsed == 0.0f) + { + if (initialFrame == Time.frameCount) + { + return true; + } + } + + elapsed += Time.deltaTime; + if (elapsed >= interval) + { + return false; + } + + return true; + } + + 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; + } + } + } + + sealed class IgnoreTimeScalePlayerLoopTimer : PlayerLoopTimer + { + int initialFrame; + float elapsed; + float interval; + + public IgnoreTimeScalePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) + { + ResetCore(interval); + } + + protected override bool MoveNextCore() + { + if (elapsed == 0.0f) + { + if (initialFrame == Time.frameCount) + { + return true; + } + } + + elapsed += Time.unscaledDeltaTime; + if (elapsed >= interval) + { + return false; + } + + return true; + } + + 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; + } + } + } + + sealed class RealtimePlayerLoopTimer : PlayerLoopTimer + { + ValueStopwatch stopwatch; + long intervalTicks; + + public RealtimePlayerLoopTimer(TimeSpan interval, bool periodic, PlayerLoopTiming playerLoopTiming, CancellationToken cancellationToken, Action timerCallback, object state) + : base(periodic, playerLoopTiming, cancellationToken, timerCallback, state) + { + ResetCore(interval); + } + + protected override bool MoveNextCore() + { + if (stopwatch.ElapsedTicks >= intervalTicks) + { + return false; + } + + return true; + } + + protected override void ResetCore(TimeSpan? interval) + { + this.stopwatch = ValueStopwatch.StartNew(); + if (interval != null) + { + this.intervalTicks = interval.Value.Ticks; + } + } + } +} + 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 6dc5517..ad95cb5 100644 --- a/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs +++ b/src/UniTask/Assets/Plugins/UniTask/Runtime/TimeoutController.cs @@ -1,8 +1,7 @@ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -using System.Threading; using System; -using Cysharp.Threading.Tasks.Internal; +using System.Threading; namespace Cysharp.Threading.Tasks { @@ -14,26 +13,44 @@ namespace Cysharp.Threading.Tasks public sealed class TimeoutController : IDisposable { + readonly static Action CancelCancellationTokenSourceStateDelegate = new Action(CancelCancellationTokenSourceState); + + static void CancelCancellationTokenSourceState(object state) + { + var cts = (CancellationTokenSource)state; + cts.Cancel(); + } + CancellationTokenSource timeoutSource; CancellationTokenSource linkedSource; - StoppableDelayRealtimePromise timeoutDelay; + PlayerLoopTimer timer; + bool isDisposed; + readonly DelayType delayType; + readonly PlayerLoopTiming delayTiming; readonly CancellationTokenSource originalLinkCancellationTokenSource; - public TimeoutController() + public TimeoutController(DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { this.timeoutSource = new CancellationTokenSource(); this.originalLinkCancellationTokenSource = null; this.linkedSource = null; - this.timeoutDelay = null; + this.delayType = delayType; + this.delayTiming = delayTiming; } - public TimeoutController(CancellationTokenSource linkCancellationTokenSource) + public TimeoutController(CancellationTokenSource linkCancellationTokenSource, DelayType delayType = DelayType.DeltaTime, PlayerLoopTiming delayTiming = PlayerLoopTiming.Update) { this.timeoutSource = new CancellationTokenSource(); this.originalLinkCancellationTokenSource = linkCancellationTokenSource; this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, linkCancellationTokenSource.Token); - this.timeoutDelay = null; + this.delayType = delayType; + this.delayTiming = delayTiming; + } + + public CancellationToken Timeout(int millisecondsTimeout) + { + return Timeout(TimeSpan.FromMilliseconds(millisecondsTimeout)); } public CancellationToken Timeout(TimeSpan timeout) @@ -43,6 +60,7 @@ namespace Cysharp.Threading.Tasks return originalLinkCancellationTokenSource.Token; } + // Timeouted, create new source and timer. if (timeoutSource.IsCancellationRequested) { timeoutSource.Dispose(); @@ -53,18 +71,25 @@ namespace Cysharp.Threading.Tasks this.linkedSource.Dispose(); this.linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, originalLinkCancellationTokenSource.Token); } + + timer?.Dispose(); + timer = null; } - if (timeoutDelay == null) + var useSource = (linkedSource != null) ? linkedSource : timeoutSource; + var token = useSource.Token; + if (timer == null) { - RunDelayAsync(timeout).Forget(); // timeoutDelay = ... in RunDelayAsync(immediately, before await) + // Timer complete => timeoutSource.Cancel() -> linkedSource will be canceled. + // (linked)token is canceled => stop timer + timer = PlayerLoopTimer.StartNew(timeout, false, delayType, delayTiming, token, CancelCancellationTokenSourceStateDelegate, timeoutSource); } else { - timeoutDelay.RestartStopwatch(); // already running RunDelayAsync + timer.Restart(timeout); } - return (linkedSource != null) ? linkedSource.Token : timeoutSource.Token; + return token; } public bool IsTimeout() @@ -74,184 +99,30 @@ namespace Cysharp.Threading.Tasks public void Reset() { - if (timeoutDelay != null) - { - timeoutDelay.Stop(); // stop delay, will finish RunDelayAsync - timeoutDelay = null; - } - } - - async UniTaskVoid RunDelayAsync(TimeSpan timeout) - { - timeoutDelay = StoppableDelayRealtimePromise.Create(timeout, PlayerLoopTiming.Update, (linkedSource == null) ? CancellationToken.None : linkedSource.Token, out var version); - try - { - var reason = await new UniTask(timeoutDelay, version); - if (reason == DelayResult.DelayCompleted) - { - // UnityEngine.Debug.Log("DEBUG:Timeout Complete, try to call timeoutSource.Cancel"); - timeoutSource.Cancel(); - } - else if (reason == DelayResult.LinkedTokenCanceled) - { - // UnityEngine.Debug.Log("DEBUG:LinkedSource IsCancellationRequested"); - } - else if (reason == DelayResult.ExternalStopped) - { - // Reset(Promise.Stop) called, do nothing. - // UnityEngine.Debug.Log("DEBUG:Reset called"); - } - } - finally - { - timeoutDelay = null; - } + timer.Stop(); } public void Dispose() { - if (timeoutDelay != null) + if (isDisposed) return; + + try { - timeoutDelay.Stop(); - } - timeoutSource.Dispose(); - if (linkedSource != null) - { - linkedSource.Dispose(); - } - } + // stop timer. + timer.Dispose(); - enum DelayResult - { - LinkedTokenCanceled, - ExternalStopped, - DelayCompleted, // as Timeout. - } - - // Stop + SuppressCancellationThrow. - sealed class StoppableDelayRealtimePromise : IUniTaskSource, IPlayerLoopItem, ITaskPoolNode - { - static OperationCanceledException ExterenalStopException = new OperationCanceledException(); - - static TaskPool pool; - StoppableDelayRealtimePromise nextNode; - public ref StoppableDelayRealtimePromise NextNode => ref nextNode; - - static StoppableDelayRealtimePromise() - { - TaskPool.RegisterSizeGetter(typeof(StoppableDelayRealtimePromise), () => pool.Size); - } - - long delayTimeSpanTicks; - ValueStopwatch stopwatch; - CancellationToken cancellationToken; - bool externalStop; - - UniTaskCompletionSourceCore core; - - StoppableDelayRealtimePromise() - { - } - - public static StoppableDelayRealtimePromise Create(TimeSpan delayTimeSpan, PlayerLoopTiming timing, CancellationToken cancellationToken, out short token) - { - if (!pool.TryPop(out var result)) + // cancel and dispose. + timeoutSource.Cancel(); + timeoutSource.Dispose(); + if (linkedSource != null) { - result = new StoppableDelayRealtimePromise(); - } - - result.stopwatch = ValueStopwatch.StartNew(); - result.delayTimeSpanTicks = delayTimeSpan.Ticks; - result.cancellationToken = cancellationToken; - result.externalStop = false; - - TaskTracker.TrackActiveTask(result, 3); - - PlayerLoopHelper.AddAction(timing, result); - - token = result.core.Version; - return result; - } - - public void Stop() - { - externalStop = true; - } - - public void RestartStopwatch() - { - stopwatch = ValueStopwatch.StartNew(); - } - - public DelayResult GetResult(short token) - { - try - { - return core.GetResult(token); - } - finally - { - TryReturn(); + linkedSource.Cancel(); + linkedSource.Dispose(); } } - - void IUniTaskSource.GetResult(short token) + finally { - GetResult(token); - } - - public UniTaskStatus GetStatus(short token) - { - return core.GetStatus(token); - } - - public UniTaskStatus UnsafeGetStatus() - { - return core.UnsafeGetStatus(); - } - - public void OnCompleted(Action continuation, object state, short token) - { - core.OnCompleted(continuation, state, token); - } - - public bool MoveNext() - { - if (cancellationToken.IsCancellationRequested) - { - core.TrySetResult(DelayResult.LinkedTokenCanceled); - return false; - } - - if (externalStop) - { - core.TrySetResult(DelayResult.ExternalStopped); - return false; - } - - if (stopwatch.IsInvalid) - { - core.TrySetResult(DelayResult.DelayCompleted); - return false; - } - - if (stopwatch.ElapsedTicks >= delayTimeSpanTicks) - { - core.TrySetResult(DelayResult.DelayCompleted); - return false; - } - - return true; - } - - bool TryReturn() - { - TaskTracker.RemoveTracking(this); - core.Reset(); - stopwatch = default; - cancellationToken = default; - externalStop = false; - return pool.TryPush(this); + isDisposed = true; } } } 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: