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.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}