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.Diagnostics.CodeAnalysis;
6using System.Threading;
7using System.Threading.Tasks;
8using NUnit.Framework;
9using osu.Framework.Allocation;
10using osu.Framework.Graphics;
11using osu.Framework.Graphics.Containers;
12using osu.Framework.Graphics.Shapes;
13using osu.Framework.Platform;
14using osu.Framework.Testing;
15using osuTK;
16using osuTK.Graphics;
17
18namespace osu.Framework.Tests.Exceptions
19{
20 [TestFixture]
21 public class TestLoadExceptions
22 {
23 [Test]
24 public void TestLoadIntoInvalidTarget()
25 {
26 var loadable = new DelayedTestBoxAsync();
27 var loadTarget = new LoadTarget(loadable);
28
29 Assert.Throws<InvalidOperationException>(() => loadTarget.PerformAsyncLoad());
30 }
31
32 [Test]
33 public void TestSingleSyncAdd()
34 {
35 var loadable = new DelayedTestBoxAsync();
36
37 Assert.DoesNotThrow(() =>
38 {
39 runGameWithLogic(g =>
40 {
41 g.Add(loadable);
42 Assert.IsTrue(loadable.LoadState == LoadState.Ready);
43 Assert.AreEqual(loadable.Parent, g);
44 g.Exit();
45 });
46 });
47 }
48
49 [Test]
50 public void TestSingleAsyncAdd()
51 {
52 var loadable = new DelayedTestBoxAsync();
53 var loadTarget = new LoadTarget(loadable);
54
55 Assert.DoesNotThrow(() =>
56 {
57 runGameWithLogic(g =>
58 {
59 g.Add(loadTarget);
60 loadTarget.PerformAsyncLoad();
61 }, g => loadable.Parent == loadTarget);
62 });
63 }
64
65 [Test]
66 public void TestDoubleAsyncLoad()
67 {
68 var loadable = new DelayedTestBoxAsync();
69 var loadTarget = new LoadTarget(loadable);
70
71 Assert.DoesNotThrow(() =>
72 {
73 runGameWithLogic(g =>
74 {
75 g.Add(loadTarget);
76 loadTarget.PerformAsyncLoad();
77 loadTarget.PerformAsyncLoad(false);
78 }, g => loadable.Parent == loadTarget);
79 });
80 }
81
82 [Test]
83 public void TestDoubleAsyncAddFails()
84 {
85 Assert.Throws<InvalidOperationException>(() =>
86 {
87 runGameWithLogic(g =>
88 {
89 var loadable = new DelayedTestBoxAsync();
90 var loadTarget = new LoadTarget(loadable);
91
92 g.Add(loadTarget);
93
94 loadTarget.PerformAsyncLoad();
95 loadTarget.PerformAsyncLoad();
96 });
97 });
98 }
99
100 [Test]
101 public void TestTargetDisposedDuringAsyncLoad()
102 {
103 Assert.Throws<ObjectDisposedException>(() =>
104 {
105 runGameWithLogic(g =>
106 {
107 var loadable = new DelayedTestBoxAsync();
108 var loadTarget = new LoadTarget(loadable);
109
110 g.Add(loadTarget);
111
112 loadTarget.PerformAsyncLoad();
113
114 while (loadable.LoadState < LoadState.Loading)
115 Thread.Sleep(1);
116
117 g.Dispose();
118 });
119 });
120 }
121
122 [Test]
123 public void TestLoadableDisposedDuringAsyncLoad()
124 {
125 Assert.Throws<ObjectDisposedException>(() =>
126 {
127 runGameWithLogic(g =>
128 {
129 var loadable = new DelayedTestBoxAsync();
130 var loadTarget = new LoadTarget(loadable);
131
132 g.Add(loadTarget);
133
134 loadTarget.PerformAsyncLoad();
135
136 while (loadable.LoadState < LoadState.Loading)
137 Thread.Sleep(1);
138
139 loadable.Dispose();
140 });
141 });
142 }
143
144 /// <summary>
145 /// The async load completion callback is scheduled on the <see cref="Game"/>. The callback is generally used to add the child to the container,
146 /// however it is possible for the container to be disposed when this occurs due to being scheduled on the <see cref="Game"/>. If this occurs,
147 /// the cancellation is invoked and the completion task should not be run.
148 ///
149 /// This is a very timing-dependent test which performs the following sequence:
150 /// LoadAsync -> schedule Callback -> dispose parent -> invoke scheduled callback
151 /// </summary>
152 [Test]
153 public void TestDisposeAfterLoad()
154 {
155 Assert.DoesNotThrow(() =>
156 {
157 var loadTarget = new LoadTarget(new DelayedTestBoxAsync());
158
159 bool allowDispose = false;
160 bool disposeTriggered = false;
161 bool updatedAfterDispose = false;
162
163 runGameWithLogic(g =>
164 {
165 g.Add(loadTarget);
166 loadTarget.PerformAsyncLoad().ContinueWith(t => allowDispose = true);
167 }, g =>
168 {
169 // The following code is done here for a very specific reason, but can occur naturally in normal use
170 // This delegate is essentially the first item in the game's scheduler, so it will always run PRIOR to the async callback
171
172 if (disposeTriggered)
173 updatedAfterDispose = true;
174
175 if (allowDispose)
176 {
177 // Async load has complete, the callback has been scheduled but NOT run yet
178 // Dispose the parent container - this is done by clearing the game
179 g.Clear(true);
180 disposeTriggered = true;
181 }
182
183 // After disposing the parent, one update loop is required
184 return updatedAfterDispose;
185 });
186 });
187 }
188
189 [Test]
190 public void TestSyncLoadException()
191 {
192 Assert.Throws<AsyncTestException>(() => runGameWithLogic(g => g.Add(new DelayedTestBoxAsync(true))));
193 }
194
195 [SuppressMessage("ReSharper", "AccessToDisposedClosure")]
196 private void runGameWithLogic(Action<Game> logic, Func<Game, bool> exitCondition = null)
197 {
198 Storage storage = null;
199
200 try
201 {
202 using (var host = new TestRunHeadlessGameHost($"{GetType().Name}-{Guid.NewGuid()}", realtime: false))
203 {
204 using (var game = new TestGame())
205 {
206 game.Schedule(() =>
207 {
208 storage = host.Storage;
209 host.UpdateThread.Scheduler.AddDelayed(() =>
210 {
211 if (exitCondition?.Invoke(game) == true)
212 host.Exit();
213 }, 0, true);
214
215 logic(game);
216 });
217
218 host.Run(game);
219 }
220 }
221 }
222 finally
223 {
224 try
225 {
226 storage?.DeleteDirectory(string.Empty);
227 }
228 catch
229 {
230 // May fail due to the file handles still being open on Windows, but this isn't a big problem for us
231 }
232 }
233 }
234
235 private class LoadTarget : Container
236 {
237 private readonly Drawable loadable;
238
239 public LoadTarget(Drawable loadable)
240 {
241 this.loadable = loadable;
242 }
243
244 public Task PerformAsyncLoad(bool withAdd = true) => LoadComponentAsync(loadable, _ =>
245 {
246 if (withAdd) Add(loadable);
247 });
248 }
249
250 public class DelayedTestBoxAsync : Box
251 {
252 private readonly bool throws;
253
254 public DelayedTestBoxAsync(bool throws = false)
255 {
256 this.throws = throws;
257 Size = new Vector2(50);
258 Colour = Color4.Green;
259 }
260
261 [BackgroundDependencyLoader]
262 private void load()
263 {
264 Task.Delay((int)(1000 / Clock.Rate)).Wait();
265 if (throws)
266 throw new AsyncTestException();
267 }
268 }
269
270 private class AsyncTestException : Exception
271 {
272 }
273 }
274}