From 3fcd28bfbd861e559c0793b60e46d56ec4a0bbd1 Mon Sep 17 00:00:00 2001 From: Bastian Blokland Date: Tue, 23 Jul 2019 22:31:18 +0300 Subject: [PATCH 1/4] Add test to verify runner destroys itself --- tests/playmode/ComponentExtensionsTests.cs | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/playmode/ComponentExtensionsTests.cs b/tests/playmode/ComponentExtensionsTests.cs index bd3a632..f7b9616 100644 --- a/tests/playmode/ComponentExtensionsTests.cs +++ b/tests/playmode/ComponentExtensionsTests.cs @@ -178,6 +178,34 @@ public IEnumerator SameRunnerIsReusedForTheSameComponent() Object.Destroy(go); } + [UnityTest] + public IEnumerator RunnerDestroysItselfWhenComponentIsDestroyed() + { + var go = new GameObject("TestGameObject"); + var comp = go.AddComponent(); + comp.StartTask(TestAsync); + + // Assert that runner was created. + Assert.AreEqual(2, go.GetComponents().Length); + + // Destroy component. + Object.DestroyImmediate(comp); + yield return null; + + // Assert that runner has destroyed itself. + Assert.AreEqual(0, go.GetComponents().Length); + + // Cleanup. + yield return null; + Object.Destroy(go); + + async Task TestAsync() + { + await Task.Yield(); + await Task.Yield(); + } + } + [UnityTest] public IEnumerator ThrowsWhenCalledFromNonUnityThread() { From 31b0797c2c379970aaa8eb99354206858d8167d8 Mon Sep 17 00:00:00 2001 From: Bastian Blokland Date: Tue, 23 Jul 2019 22:48:10 +0300 Subject: [PATCH 2/4] Restroy task runner when work is done --- .../Internal/MonoBehaviourTaskRunner.cs | 83 ++++++++++++++----- src/ComponentTask/LocalTaskRunner.cs | 8 +- tests/playmode/ComponentExtensionsTests.cs | 26 ++++++ 3 files changed, 96 insertions(+), 21 deletions(-) diff --git a/src/ComponentTask/Internal/MonoBehaviourTaskRunner.cs b/src/ComponentTask/Internal/MonoBehaviourTaskRunner.cs index c0982e9..aafe3ac 100644 --- a/src/ComponentTask/Internal/MonoBehaviourTaskRunner.cs +++ b/src/ComponentTask/Internal/MonoBehaviourTaskRunner.cs @@ -15,56 +15,84 @@ public MonoBehaviourTaskRunner() public UnityEngine.Component ComponentToFollow { get; set; } - public Task StartTask(Func taskCreator) => - this.taskRunner.StartTask(taskCreator); + public bool IsFinished { get; private set; } - public Task StartTask(Func taskCreator) => - this.taskRunner.StartTask(taskCreator); + public Task StartTask(Func taskCreator) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator); + } - public Task StartTask(Func taskCreator, TIn data) => - this.taskRunner.StartTask(taskCreator, data); + public Task StartTask(Func taskCreator) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator); + } - public Task StartTask(Func taskCreator, TIn data) => - this.taskRunner.StartTask(taskCreator, data); + public Task StartTask(Func taskCreator, TIn data) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator, data); + } - public Task StartTask(Func> taskCreator) => - this.taskRunner.StartTask(taskCreator); + public Task StartTask(Func taskCreator, TIn data) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator, data); + } - public Task StartTask(Func> taskCreator) => - this.taskRunner.StartTask(taskCreator); + public Task StartTask(Func> taskCreator) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator); + } - public Task StartTask(Func> taskCreator, TIn data) => - this.taskRunner.StartTask(taskCreator, data); + public Task StartTask(Func> taskCreator) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator); + } - public Task StartTask(Func> taskCreator, TIn data) => - this.taskRunner.StartTask(taskCreator, data); + public Task StartTask(Func> taskCreator, TIn data) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator, data); + } + + public Task StartTask(Func> taskCreator, TIn data) + { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Task was started on already finished runner"); + return this.taskRunner.StartTask(taskCreator, data); + } // Dynamically called from the Unity runtime. private void LateUpdate() { + System.Diagnostics.Debug.Assert(!this.IsFinished, "Already finished runner was updated"); + // Check if we have a 'ComponentToFollow' assigned. if (this.ComponentToFollow is null) { // If not then always just execute the runner. - this.taskRunner.Execute(); + this.Execute(); } else { // If the component we are following has been destroyed then we destroy ourselves. if (!this.ComponentToFollow) - UnityEngine.Object.Destroy(this); + this.Destroy(); else { // If the component is a 'Behaviour' then we update when its enabled. if (ComponentToFollow is UnityEngine.Behaviour behaviour) { if (behaviour.isActiveAndEnabled) - this.taskRunner.Execute(); + this.Execute(); } else { // Otherwise we always update. - this.taskRunner.Execute(); + this.Execute(); } } } @@ -73,6 +101,21 @@ private void LateUpdate() // Dynamically called from the Unity runtime. private void OnDestroy() => this.taskRunner.Dispose(); + private void Execute() + { + var workRemaining = this.taskRunner.Execute(); + + // If we've finished all the work then destroy ourselves. + if (!workRemaining) + this.Destroy(); + } + + private void Destroy() + { + this.IsFinished = true; + UnityEngine.Object.Destroy(this); + } + void IExceptionHandler.Handle(Exception exception) { if (exception is null) diff --git a/src/ComponentTask/LocalTaskRunner.cs b/src/ComponentTask/LocalTaskRunner.cs index e92a16a..541a790 100644 --- a/src/ComponentTask/LocalTaskRunner.cs +++ b/src/ComponentTask/LocalTaskRunner.cs @@ -169,11 +169,13 @@ public Task StartTask(Func> /// /// Execute all the work that was 'scheduled' by the tasks running on this runner. /// - public void Execute() + /// True if still running work, False if all work has finished. + public bool Execute() { if (this.isDisposed) throw new ObjectDisposedException(nameof(LocalTaskRunner)); + bool tasksRemaining; try { // Execute all the work that was scheduled on this runner. @@ -192,8 +194,12 @@ public void Execute() if (this.runningTasks[i].IsFinished) this.runningTasks.RemoveAt(i); } + + tasksRemaining = this.runningTasks.Count > 0; } } + + return tasksRemaining; } /// diff --git a/tests/playmode/ComponentExtensionsTests.cs b/tests/playmode/ComponentExtensionsTests.cs index f7b9616..a1f95c8 100644 --- a/tests/playmode/ComponentExtensionsTests.cs +++ b/tests/playmode/ComponentExtensionsTests.cs @@ -206,6 +206,32 @@ async Task TestAsync() } } + [UnityTest] + public IEnumerator RunnerDestroysItselfWhenWorkIsDone() + { + var go = new GameObject("TestGameObject"); + var comp = go.AddComponent(); + comp.StartTask(TestAsync); + + // Assert that runner was created. + Assert.AreEqual(2, go.GetComponents().Length); + + // Wait for work to finish. + yield return null; + + // Assert that runner has destroyed itself. + Assert.AreEqual(1, go.GetComponents().Length); + + // Cleanup. + yield return null; + Object.Destroy(go); + + async Task TestAsync() + { + await Task.Yield(); + } + } + [UnityTest] public IEnumerator ThrowsWhenCalledFromNonUnityThread() { From bd83ba0b64c2c111b8eb180690ab57dc55ac518c Mon Sep 17 00:00:00 2001 From: Bastian Blokland Date: Tue, 23 Jul 2019 22:51:54 +0300 Subject: [PATCH 3/4] Add test to verify task pausing --- tests/playmode/ComponentExtensionsTests.cs | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/playmode/ComponentExtensionsTests.cs b/tests/playmode/ComponentExtensionsTests.cs index a1f95c8..d71f007 100644 --- a/tests/playmode/ComponentExtensionsTests.cs +++ b/tests/playmode/ComponentExtensionsTests.cs @@ -164,6 +164,50 @@ async Task IncrementCountAsync() } } + [UnityTest] + public IEnumerator TaskPausesWhenComponentIsDisabled() + { + var count = 0; + var go = new GameObject("TestGameObject"); + var comp = go.AddComponent(); + comp.StartTask(IncrementCountAsync); + + // Assert task is running. + yield return null; + Assert.AreEqual(1, count); + + // Disable component. + comp.enabled = false; + + // Assert task is paused. + yield return null; + Assert.AreEqual(1, count); + yield return null; + Assert.AreEqual(1, count); + + // Enable component. + comp.enabled = true; + + // Assert task is running. + yield return null; + Assert.AreEqual(2, count); + yield return null; + Assert.AreEqual(3, count); + + // Cleanup. + Object.Destroy(go); + + async Task IncrementCountAsync() + { + await Task.Yield(); + count++; + await Task.Yield(); + count++; + await Task.Yield(); + count++; + } + } + [UnityTest] public IEnumerator SameRunnerIsReusedForTheSameComponent() { From 4f0e866395353208907893f6c386f23ea241972a Mon Sep 17 00:00:00 2001 From: Bastian Blokland Date: Tue, 23 Jul 2019 22:53:49 +0300 Subject: [PATCH 4/4] Increase test coverage --- tests/playmode/ComponentExtensionsTests.cs | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/playmode/ComponentExtensionsTests.cs b/tests/playmode/ComponentExtensionsTests.cs index d71f007..400e6c8 100644 --- a/tests/playmode/ComponentExtensionsTests.cs +++ b/tests/playmode/ComponentExtensionsTests.cs @@ -240,7 +240,36 @@ public IEnumerator RunnerDestroysItselfWhenComponentIsDestroyed() Assert.AreEqual(0, go.GetComponents().Length); // Cleanup. + Object.Destroy(go); + + async Task TestAsync() + { + await Task.Yield(); + await Task.Yield(); + } + } + + [UnityTest] + public IEnumerator RunnerDestroysItselfWhenDisabledComponentIsDestroyed() + { + var go = new GameObject("TestGameObject"); + var comp = go.AddComponent(); + comp.StartTask(TestAsync); + + // Disable component. + comp.enabled = false; + + // Assert that runner was created. + Assert.AreEqual(2, go.GetComponents().Length); + + // Destroy component. + Object.DestroyImmediate(comp); yield return null; + + // Assert that runner has destroyed itself. + Assert.AreEqual(0, go.GetComponents().Length); + + // Cleanup. Object.Destroy(go); async Task TestAsync()