A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Generic;
6using System.Threading;
7using NUnit.Framework;
8using osu.Framework.Allocation;
9using osu.Framework.Graphics;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Testing;
12using osu.Framework.Tests.Visual;
13using osu.Framework.Threading;
14
15namespace osu.Framework.Tests.Containers
16{
17 [HeadlessTest]
18 public class TestSceneLongRunningLoad : FrameworkTestScene
19 {
20 /// <summary>
21 /// Tests that an exception is thrown when a long-running drawable is synchronously loaded.
22 /// </summary>
23 [Test]
24 public void TestSynchronousLoadLongRunningThrows() => testSynchronousLoad(() => new TestLoadBlockingDrawableLongRunning(true), true);
25
26 /// <summary>
27 /// Tests that an exception is not thrown when a long-running drawable is asynchronously loaded.
28 /// </summary>
29 [Test]
30 public void TestAsynchronousLoadLongRunningDoesNotThrow() => testAsynchronousLoad(() => new TestLoadBlockingDrawableLongRunning(true), false);
31
32 /// <summary>
33 /// Tests that an exception is thrown when a derived long-running drawable is synchronously loaded.
34 /// </summary>
35 [Test]
36 public void TestSynchronousLoadDerivedLongRunningThrows() => testSynchronousLoad(() => new TestLoadBlockingDrawableLongRunningDerived(true), true);
37
38 /// <summary>
39 /// Tests that an exception is not thrown when a derived long-running drawable is asynchronously loaded.
40 /// </summary>
41 [Test]
42 public void TestAsynchronousLoadDerivedLongRunningDoesNotThrow() => testAsynchronousLoad(() => new TestLoadBlockingDrawableLongRunningDerived(true), false);
43
44 /// <summary>
45 /// Tests that an exception is thrown when a parent is synchronously loaded and contains a long-running child.
46 /// </summary>
47 [Test]
48 public void TestLoadParentSynchronousThrows() => testSynchronousLoad(() => new Container
49 {
50 Child = new TestLoadBlockingDrawableLongRunningDerived(true)
51 }, true);
52
53 /// <summary>
54 /// Tests that an exception is thrown when a parent is asynchronously loaded and contains a long-running child.
55 /// </summary>
56 [Test]
57 public void TestLoadParentAsynchronousThrows() => testAsynchronousLoad(() => new Container
58 {
59 Child = new TestLoadBlockingDrawableLongRunningDerived(true)
60 }, true);
61
62 /// <summary>
63 /// Tests that long-running drawables don't block non-long running drawables from loading.
64 /// </summary>
65 [Test]
66 public void TestLongRunningLoadDoesNotBlock()
67 {
68 List<TestLoadBlockingDrawableLongRunning> longRunning = new List<TestLoadBlockingDrawableLongRunning>();
69
70 // add enough drawables to saturate the task scheduler
71 AddRepeatStep("add long running", () =>
72 {
73 var d = new TestLoadBlockingDrawableLongRunning();
74 longRunning.Add(d);
75 LoadComponentAsync(d);
76 }, 10);
77
78 TestLoadBlockingDrawable normal = null;
79
80 AddStep("add normal", () => { LoadComponentAsync(normal = new TestLoadBlockingDrawable(), Add); });
81 AddStep("allow normal load", () => normal.AllowLoad.Set());
82 AddUntilStep("did load", () => normal.IsLoaded);
83
84 AddStep("allow long running load", () => longRunning.ForEach(d => d.AllowLoad.Set()));
85 }
86
87 private void testSynchronousLoad(Func<Drawable> context, bool shouldThrow)
88 {
89 AddAssert($"{(shouldThrow ? "has" : "has not")} thrown", () =>
90 {
91 try
92 {
93 Add(context());
94 }
95 catch (InvalidOperationException)
96 {
97 return shouldThrow;
98 }
99
100 return !shouldThrow;
101 });
102 }
103
104 private void testAsynchronousLoad(Func<Drawable> context, bool shouldThrow)
105 {
106 Scheduler scheduler = null;
107
108 AddStep("begin long running", () => LoadComponentAsync(context(), scheduler: scheduler = new Scheduler()));
109
110 // Exceptions during async loads are thrown on the scheduler rather than on invocation
111 AddUntilStep("wait for load to complete", () => scheduler.HasPendingTasks);
112
113 AddAssert($"{(shouldThrow ? "has" : "has not")} thrown", () =>
114 {
115 try
116 {
117 scheduler.Update();
118 }
119 catch (InvalidOperationException)
120 {
121 return shouldThrow;
122 }
123
124 return !shouldThrow;
125 });
126 }
127
128 private class TestLoadBlockingDrawableLongRunningDerived : TestLoadBlockingDrawableLongRunning
129 {
130 public TestLoadBlockingDrawableLongRunningDerived(bool allowLoad = false)
131 : base(allowLoad)
132 {
133 }
134 }
135
136 [LongRunningLoad]
137 private class TestLoadBlockingDrawableLongRunning : CompositeDrawable
138 {
139 public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim();
140
141 public TestLoadBlockingDrawableLongRunning(bool allowLoad = false)
142 {
143 if (allowLoad)
144 AllowLoad.Set();
145 }
146
147 [BackgroundDependencyLoader]
148 private void load()
149 {
150 if (!AllowLoad.Wait(TimeSpan.FromSeconds(10)))
151 throw new TimeoutException();
152 }
153 }
154
155 private class TestLoadBlockingDrawable : CompositeDrawable
156 {
157 public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim();
158
159 public TestLoadBlockingDrawable(bool allowLoad = false)
160 {
161 if (allowLoad)
162 AllowLoad.Set();
163 }
164
165 [BackgroundDependencyLoader]
166 private void load()
167 {
168 if (!AllowLoad.Wait(TimeSpan.FromSeconds(10)))
169 throw new TimeoutException();
170 }
171 }
172 }
173}