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.Linq;
7using System.Threading;
8using NUnit.Framework;
9using osu.Framework.Allocation;
10using osu.Framework.Graphics;
11using osu.Framework.Graphics.Containers;
12using osu.Framework.Testing;
13using osu.Framework.Tests.Visual;
14
15namespace osu.Framework.Tests.Containers
16{
17 [HeadlessTest]
18 public class TestSceneLoadComponentAsync : FrameworkTestScene
19 {
20 [Test]
21 public void TestEnumerableOnlyInvokedOnce()
22 {
23 int invocationCount = 0;
24
25 IEnumerable<AsyncChildLoadingComposite> composite = getEnumerableComponent(() =>
26 {
27 invocationCount++;
28
29 var result = new AsyncChildLoadingComposite();
30 result.AllowChildLoad();
31
32 return result;
33 });
34
35 AddStep("clear all children", () => Clear());
36
37 AddStep("load async", () => LoadComponentsAsync(composite, AddRange));
38
39 AddUntilStep("component loaded", () => Children.Count == 1);
40
41 AddAssert("invocation count is 1", () => invocationCount == 1);
42 }
43
44 private IEnumerable<AsyncChildLoadingComposite> getEnumerableComponent(Func<AsyncChildLoadingComposite> createComponent)
45 {
46 yield return createComponent();
47 }
48
49 [Test]
50 public void TestUnpublishedChildDisposal()
51 {
52 AsyncChildLoadingComposite composite = null;
53
54 AddStep("Add new composite", () => { Child = composite = new AsyncChildLoadingComposite(); });
55
56 AddStep("Allow load", () => composite.AllowChildLoad());
57
58 AddUntilStep("Wait for child load", () => composite.AsyncChild.LoadState == LoadState.Ready);
59
60 AddStep("Dispose composite", Clear);
61
62 AddUntilStep("Child was disposed", () => composite.AsyncChildDisposed);
63 }
64
65 [Test]
66 public void TestUnpublishedChildLoadBlockDisposal()
67 {
68 AsyncChildLoadingComposite composite = null;
69
70 AddStep("Add new composite", () => { Child = composite = new AsyncChildLoadingComposite(); });
71
72 AddUntilStep("Wait for child load began", () => composite.AsyncChildLoadBegan);
73
74 AddStep("Dispose composite", Clear);
75
76 AddWaitStep("Wait for potential disposal", 50);
77
78 AddAssert("Composite not yet disposed", () => !composite.IsDisposed);
79
80 AddAssert("Child not yet disposed", () => !composite.AsyncChildDisposed);
81
82 AddStep("Allow load", () => composite.AllowChildLoad());
83
84 AddUntilStep("Wait for child load", () => composite.AsyncChild.LoadState == LoadState.Ready);
85
86 AddUntilStep("Composite was disposed", () => composite.IsDisposed);
87
88 AddUntilStep("Child was disposed", () => composite.AsyncChildDisposed);
89 }
90
91 [Test]
92 public void TestDisposalDuringAsyncLoad()
93 {
94 AsyncChildrenLoadingComposite composite = null;
95
96 AddStep("Add new composite", () => { Child = composite = new AsyncChildrenLoadingComposite(); });
97
98 AddStep("Dispose child 2", () => composite.AsyncChild2.Dispose());
99
100 AddStep("Allow child 1 load", () => composite.AllowChild1Load());
101
102 AddUntilStep("Wait for child load", () => composite.AsyncChild1.LoadState == LoadState.Ready);
103
104 AddUntilStep("Wait for loaded callback", () => composite.LoadedChildren != null);
105
106 AddAssert("Only child1 loaded", () => composite.LoadedChildren.Count() == 1
107 && composite.LoadedChildren.First() == composite.AsyncChild1);
108 }
109
110 [Test]
111 public void TestScheduleDuringAsyncLoad()
112 {
113 TestLoadBlockingDrawable composite = null;
114
115 bool scheduleRun = false;
116
117 AddStep("Async load drawable", () =>
118 {
119 LoadComponentAsync(composite = new TestLoadBlockingDrawable(), d => Child = d);
120 });
121
122 AddStep("Attempt to schedule on child 1", () =>
123 {
124 composite.Schedule(() => scheduleRun = true);
125 });
126
127 AddStep("Allow child 1 load", () => composite.AllowLoad.Set());
128
129 AddUntilStep("Scheduled content run", () => scheduleRun);
130 }
131
132 private class AsyncChildrenLoadingComposite : CompositeDrawable
133 {
134 public IEnumerable<TestLoadBlockingDrawable> LoadedChildren;
135
136 public TestLoadBlockingDrawable AsyncChild1 { get; } = new TestLoadBlockingDrawable();
137
138 public TestLoadBlockingDrawable AsyncChild2 { get; } = new TestLoadBlockingDrawable();
139
140 public bool AsyncChild1LoadBegan => AsyncChild1.LoadState > LoadState.NotLoaded;
141
142 public void AllowChild1Load() => AsyncChild1.AllowLoad.Set();
143
144 public void AllowChild2Load() => AsyncChild2.AllowLoad.Set();
145
146 public new bool IsDisposed => base.IsDisposed;
147
148 protected override void LoadComplete()
149 {
150 // load but never add to hierarchy
151 LoadComponentsAsync(new[] { AsyncChild1, AsyncChild2 }, loadComplete);
152
153 base.LoadComplete();
154 }
155
156 private void loadComplete(IEnumerable<TestLoadBlockingDrawable> loadedChildren) => LoadedChildren = loadedChildren;
157 }
158
159 private class AsyncChildLoadingComposite : CompositeDrawable
160 {
161 public TestLoadBlockingDrawable AsyncChild { get; } = new TestLoadBlockingDrawable();
162
163 public bool AsyncChildDisposed { get; private set; }
164
165 public bool AsyncChildLoadBegan => AsyncChild.LoadState > LoadState.NotLoaded;
166
167 public void AllowChildLoad() => AsyncChild.AllowLoad.Set();
168
169 public new bool IsDisposed => base.IsDisposed;
170
171 protected override void LoadComplete()
172 {
173 AsyncChild.OnDispose += () => AsyncChildDisposed = true;
174
175 // load but never add to hierarchy
176 LoadComponentAsync(AsyncChild);
177
178 base.LoadComplete();
179 }
180 }
181
182 private class TestLoadBlockingDrawable : CompositeDrawable
183 {
184 public readonly ManualResetEventSlim AllowLoad = new ManualResetEventSlim();
185
186 [BackgroundDependencyLoader]
187 private void load()
188 {
189 if (!AllowLoad.Wait(TimeSpan.FromSeconds(10)))
190 throw new TimeoutException();
191 }
192 }
193 }
194}