A game framework written with osu! in mind.
at master 358 lines 12 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.Linq; 6using System.Threading; 7using NUnit.Framework; 8using osu.Framework.Allocation; 9using osu.Framework.Graphics; 10using osu.Framework.Graphics.Containers; 11using osu.Framework.Graphics.Shapes; 12using osu.Framework.Graphics.Sprites; 13using osu.Framework.Testing; 14using osu.Framework.Tests.Visual; 15using osuTK; 16 17namespace osu.Framework.Tests.Containers 18{ 19 [System.ComponentModel.Description("ensure valid container state in various scenarios")] 20 [HeadlessTest] 21 public class TestSceneContainerState : FrameworkTestScene 22 { 23 /// <summary> 24 /// Tests if a drawable can be added to a container, removed, and then re-added to the same container. 25 /// </summary> 26 [Test] 27 public void TestPreLoadReAdding() 28 { 29 var container = new Container(); 30 var sprite = new Sprite(); 31 32 // Add 33 Assert.DoesNotThrow(() => container.Add(sprite)); 34 Assert.IsTrue(container.Contains(sprite)); 35 36 // Remove 37 Assert.DoesNotThrow(() => container.Remove(sprite)); 38 Assert.IsFalse(container.Contains(sprite)); 39 40 // Re-add 41 Assert.DoesNotThrow(() => container.Add(sprite)); 42 Assert.IsTrue(container.Contains(sprite)); 43 } 44 45 /// <summary> 46 /// Tests whether adding a child to multiple containers results in a <see cref="InvalidOperationException"/>. 47 /// </summary> 48 [Test] 49 public void TestPreLoadMultipleAdds() 50 { 51 // Non-async 52 Assert.Throws<InvalidOperationException>(() => 53 { 54 var unused1 = new Container 55 { 56 Child = new Container(), 57 }; 58 59 var unused2 = new Container { Child = unused1.Child }; 60 }); 61 } 62 63 /// <summary> 64 /// The same as <see cref="TestPreLoadMultipleAdds"/> however instead runs after the container is loaded. 65 /// </summary> 66 [Test] 67 public void TestLoadedMultipleAdds() 68 { 69 AddAssert("Test loaded multiple adds", () => 70 { 71 var loadedContainer = new Container(); 72 Add(loadedContainer); 73 74 try 75 { 76 var unused = new Container 77 { 78 Child = new Container(), 79 }; 80 81 loadedContainer.Add(new Container { Child = unused.Child }); 82 return false; 83 } 84 catch (InvalidOperationException) 85 { 86 return true; 87 } 88 }); 89 } 90 91 /// <summary> 92 /// Tests whether a drawable that is loaded can be added to an unloaded container. 93 /// </summary> 94 [Test] 95 public void TestAddLoadedDrawableToUnloadedContainer() 96 { 97 Drawable target = null; 98 99 AddStep("load target", () => 100 { 101 Add(target = new Box { Size = new Vector2(100) }); 102 103 // Empty scheduler to force creation of the scheduler. 104 target.Schedule(() => { }); 105 }); 106 107 AddStep("remove target", () => Remove(target)); 108 AddStep("add target to unloaded container", () => Add(new Container { Child = target })); 109 } 110 111 /// <summary> 112 /// Tests whether the result of a <see cref="Container{T}.Contains(T)"/> operation is valid between multiple containers. 113 /// This tests whether the comparator + equality operation in <see cref="CompositeDrawable.IndexOfInternal(Drawable)"/> is valid. 114 /// </summary> 115 [Test] 116 public void TestContainerContains() 117 { 118 var drawableA = new Sprite(); 119 var drawableB = new Sprite(); 120 var containerA = new Container { Child = drawableA }; 121 var containerB = new Container { Child = drawableB }; 122 123 var newContainer = new Container<Container> { Children = new[] { containerA, containerB } }; 124 125 // Because drawableA and drawableB have been added to separate containers, 126 // they will both have Depth = 0 and ChildID = 1, which leads to edge cases if a 127 // sorting comparer that doesn't compare references is used for Contains(). 128 // If this is not handled properly, it may have devastating effects in, e.g. Remove(). 129 130 Assert.IsTrue(newContainer.First(c => c.Contains(drawableA)) == containerA); 131 Assert.IsTrue(newContainer.First(c => c.Contains(drawableB)) == containerB); 132 133 Assert.DoesNotThrow(() => newContainer.First(c => c.Contains(drawableA)).Remove(drawableA)); 134 Assert.DoesNotThrow(() => newContainer.First(c => c.Contains(drawableB)).Remove(drawableB)); 135 } 136 137 [Test] 138 public void TestChildrenRemovedOnClearInternal() 139 { 140 var drawableA = new Sprite(); 141 var drawableB = new Sprite(); 142 var drawableC = new Sprite(); 143 var containerA = new Container { Child = drawableC }; 144 145 var targetContainer = new Container { Children = new Drawable[] { drawableA, drawableB, containerA } }; 146 147 Assert.That(targetContainer, Has.Count.Not.Zero); 148 149 targetContainer.ClearInternal(); 150 151 // Immediate children removed 152 Assert.That(targetContainer, Has.Count.Zero); 153 154 // Nested container's children not removed 155 Assert.That(containerA, Has.Count.EqualTo(1)); 156 } 157 158 [TestCase(false)] 159 [TestCase(true)] 160 public void TestUnbindOnClearInternal(bool shouldDispose) 161 { 162 bool unbound = false; 163 164 var drawableA = new Sprite().With(d => { d.OnUnbindAllBindables += () => unbound = true; }); 165 166 var container = new Container { Children = new[] { drawableA } }; 167 168 container.ClearInternal(shouldDispose); 169 170 Assert.That(container, Has.Count.Zero); 171 Assert.That(unbound, Is.EqualTo(shouldDispose)); 172 173 GC.KeepAlive(drawableA); 174 } 175 176 [TestCase(false)] 177 [TestCase(true)] 178 public void TestDisposeOnClearInternal(bool shouldDispose) 179 { 180 bool disposed = false; 181 182 var drawableA = new Sprite().With(d => { d.OnDispose += () => disposed = true; }); 183 184 var container = new Container { Children = new[] { drawableA } }; 185 186 Assert.That(container, Has.Count.Not.Zero); 187 188 container.ClearInternal(shouldDispose); 189 190 Assert.That(container, Has.Count.Zero); 191 192 // Disposal happens asynchronously 193 int iterations = 20; 194 195 while (iterations-- > 0) 196 { 197 if (disposed) 198 break; 199 200 Thread.Sleep(100); 201 } 202 203 Assert.That(disposed, Is.EqualTo(shouldDispose)); 204 205 GC.KeepAlive(drawableA); 206 } 207 208 [Test] 209 public void TestAsyncLoadClearWhileAsyncDisposing() 210 { 211 Container safeContainer = null; 212 DelayedLoadDrawable drawable = null; 213 214 // We are testing a disposal deadlock scenario. When the test runner exits, it will attempt to dispose the game hierarchy, 215 // and will fall into the deadlocked state itself. For this reason an intermediate "safe" container is used, which is 216 // removed from the hierarchy immediately after use and is thus not disposed when the test runner exits. 217 // This does NOT free up the LoadComponentAsync thread pool for use by other tests - that thread is in a deadlocked state forever. 218 AddStep("add safe container", () => Add(safeContainer = new Container())); 219 220 // Get the drawable into an async loading state 221 AddStep("begin async load", () => 222 { 223 safeContainer.LoadComponentAsync(drawable = new DelayedLoadDrawable(), _ => { }); 224 Remove(safeContainer); 225 }); 226 227 AddUntilStep("wait until loading", () => drawable.LoadState == LoadState.Loading); 228 229 // Make the async disposal queue attempt to dispose the drawable 230 AddStep("enqueue async disposal", () => AsyncDisposalQueue.Enqueue(drawable)); 231 AddWaitStep("wait for disposal task to run", 10); 232 233 // Clear the contents of the drawable, causing a second async disposal 234 AddStep("allow load", () => drawable.AllowLoad.Set()); 235 236 AddUntilStep("drawable was cleared successfully", () => drawable.HasCleared); 237 } 238 239 [Test] 240 public void TestExpireChildAfterLoad() 241 { 242 Container container = null; 243 Drawable child = null; 244 245 AddStep("add container and child", () => 246 { 247 Add(container = new Container 248 { 249 Child = child = new Box() 250 }); 251 }); 252 253 AddStep("expire child", () => child.Expire()); 254 255 AddUntilStep("container has no children", () => container.Count == 0); 256 } 257 258 [Test] 259 public void TestExpireChildBeforeLoad() 260 { 261 Container container = null; 262 263 AddStep("add container", () => Add(container = new Container())); 264 265 AddStep("add expired child", () => 266 { 267 var child = new Box(); 268 child.Expire(); 269 270 container.Add(child); 271 }); 272 273 AddUntilStep("container has no children", () => container.Count == 0); 274 } 275 276 private class DelayedLoadDrawable : CompositeDrawable 277 { 278 public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim(); 279 280 public bool HasCleared { get; private set; } 281 282 public DelayedLoadDrawable() 283 { 284 InternalChild = new Box(); 285 } 286 287 [BackgroundDependencyLoader] 288 private void load() 289 { 290 if (!AllowLoad.Wait(TimeSpan.FromSeconds(10))) 291 throw new TimeoutException(); 292 293 ClearInternal(); 294 295 HasCleared = true; 296 } 297 } 298 299 [Test] 300 public void TestAliveChangesDuringExpiry() 301 { 302 TestContainer container = null; 303 304 int count = 0; 305 306 void checkCount() => count = container.AliveInternalChildren.Count; 307 308 AddStep("create container", () => Child = container = new TestContainer()); 309 310 AddStep("perform test", () => 311 { 312 container.Add(new Box()); 313 container.Add(new Box()); 314 container.ScheduleAfterChildren(checkCount); 315 }); 316 317 AddAssert("correct count", () => count == 2); 318 319 AddStep("perform test", () => 320 { 321 container.First().Expire(); 322 container.Add(new Box()); 323 container.ScheduleAfterChildren(checkCount); 324 }); 325 326 AddAssert("correct count", () => count == 2); 327 } 328 329 [Test] 330 public void TestAliveChildrenContainsOnlyAliveChildren() 331 { 332 Container container = null; 333 Drawable aliveChild = null; 334 Drawable nonAliveChild = null; 335 336 AddStep("create container", () => 337 { 338 Child = container = new Container 339 { 340 Children = new[] 341 { 342 aliveChild = new Box(), 343 nonAliveChild = new Box { LifetimeStart = double.MaxValue } 344 } 345 }; 346 }); 347 348 AddAssert("1 alive child", () => container.AliveChildren.Count == 1); 349 AddAssert("alive child contained", () => container.AliveChildren.Contains(aliveChild)); 350 AddAssert("non-alive child not contained", () => !container.AliveChildren.Contains(nonAliveChild)); 351 } 352 353 private class TestContainer : Container 354 { 355 public new void ScheduleAfterChildren(Action action) => SchedulerAfterChildren.AddDelayed(action, TransformDelay); 356 } 357 } 358}