A game framework written with osu! in mind.
at master 8.8 kB view raw
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}