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 NUnit.Framework;
8using osu.Framework.Graphics.Performance;
9
10namespace osu.Framework.Tests.Graphics
11{
12 [TestFixture]
13 public class LifetimeEntryManagerTest
14 {
15 private TestLifetimeEntryManager manager;
16
17 [SetUp]
18 public void Setup()
19 {
20 manager = new TestLifetimeEntryManager();
21 }
22
23 [Test]
24 public void TestBasic()
25 {
26 manager.AddEntry(new LifetimeEntry { LifetimeStart = -1, LifetimeEnd = 1 });
27 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 });
28 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 2 });
29 manager.AddEntry(new LifetimeEntry { LifetimeStart = 1, LifetimeEnd = 2 });
30 manager.AddEntry(new LifetimeEntry { LifetimeStart = 2, LifetimeEnd = 2 });
31 manager.AddEntry(new LifetimeEntry { LifetimeStart = 2, LifetimeEnd = 3 });
32
33 checkCountAliveAt(0, 3);
34 checkCountAliveAt(1, 2);
35 checkCountAliveAt(2, 1);
36 checkCountAliveAt(0, 3);
37 checkCountAliveAt(3, 0);
38 }
39
40 [Test]
41 public void TestRemoveAndReAdd()
42 {
43 var entry = new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 };
44
45 manager.AddEntry(entry);
46 checkCountAliveAt(0, 1);
47
48 manager.RemoveEntry(entry);
49 checkCountAliveAt(0, 0);
50
51 manager.AddEntry(entry);
52 checkCountAliveAt(0, 1);
53 }
54
55 [Test]
56 public void TestDynamicChange()
57 {
58 manager.AddEntry(new LifetimeEntry { LifetimeStart = -1, LifetimeEnd = 0 });
59 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 });
60 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 });
61 manager.AddEntry(new LifetimeEntry { LifetimeStart = 1, LifetimeEnd = 2 });
62
63 checkCountAliveAt(0, 2);
64
65 manager.Entries[0].LifetimeEnd = 1;
66 manager.Entries[1].LifetimeStart = 1;
67 manager.Entries[2].LifetimeEnd = 0;
68 manager.Entries[3].LifetimeStart = 0;
69
70 checkCountAliveAt(0, 2);
71
72 foreach (var entry in manager.Entries)
73 {
74 entry.LifetimeEnd += 1;
75 entry.LifetimeStart += 1;
76 }
77
78 checkCountAliveAt(0, 1);
79
80 foreach (var entry in manager.Entries)
81 {
82 entry.LifetimeStart -= 1;
83 entry.LifetimeEnd -= 1;
84 }
85
86 checkCountAliveAt(0, 2);
87 }
88
89 [Test]
90 public void TestBoundaryCrossing()
91 {
92 manager.AddEntry(new LifetimeEntry { LifetimeStart = -1, LifetimeEnd = 0 });
93 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 });
94 manager.AddEntry(new LifetimeEntry { LifetimeStart = 1, LifetimeEnd = 2 });
95
96 // No crossings for the first update.
97 manager.Update(0);
98 checkNoCrossing(manager.Entries[0]);
99 checkNoCrossing(manager.Entries[1]);
100 checkNoCrossing(manager.Entries[2]);
101
102 manager.Update(2);
103 checkNoCrossing(manager.Entries[0]);
104 checkCrossing(manager.Entries[1], 0, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward);
105 checkCrossing(manager.Entries[2], 0, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Forward);
106 checkCrossing(manager.Entries[2], 1, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Forward);
107
108 manager.Update(1);
109 checkNoCrossing(manager.Entries[0]);
110 checkNoCrossing(manager.Entries[1]);
111 checkCrossing(manager.Entries[2], 0, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward);
112
113 manager.Update(-1);
114 checkCrossing(manager.Entries[0], 0, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward);
115 checkCrossing(manager.Entries[1], 0, LifetimeBoundaryKind.End, LifetimeBoundaryCrossingDirection.Backward);
116 checkCrossing(manager.Entries[1], 1, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward);
117 checkCrossing(manager.Entries[2], 0, LifetimeBoundaryKind.Start, LifetimeBoundaryCrossingDirection.Backward);
118 }
119
120 [Test]
121 public void TestLifetimeChangeOnCallback()
122 {
123 int updateTime = 0;
124
125 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 });
126 manager.EntryCrossedBoundary += (entry, kind, direction) =>
127 {
128 switch (kind)
129 {
130 case LifetimeBoundaryKind.End when direction == LifetimeBoundaryCrossingDirection.Forward:
131 entry.LifetimeEnd = 2;
132 break;
133
134 case LifetimeBoundaryKind.Start when direction == LifetimeBoundaryCrossingDirection.Backward:
135 entry.LifetimeEnd = 1;
136 break;
137
138 case LifetimeBoundaryKind.Start when direction == LifetimeBoundaryCrossingDirection.Forward:
139 entry.LifetimeStart = entry.LifetimeStart == 0 ? 1 : 0;
140 break;
141 }
142
143 // Lifetime changes are applied in the _next_ update.
144 // ReSharper disable once AccessToModifiedClosure - intentional.
145 manager.Update(updateTime);
146 };
147
148 manager.Update(updateTime = 0);
149
150 checkCountAliveAt(updateTime = 1, 1);
151 checkCountAliveAt(updateTime = -1, 0);
152 checkCountAliveAt(updateTime = 0, 0);
153 checkCountAliveAt(updateTime = 1, 1);
154 }
155
156 [Test]
157 public void TestUpdateWithTimeRange()
158 {
159 manager.AddEntry(new LifetimeEntry { LifetimeStart = -1, LifetimeEnd = 1 });
160 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 1 });
161 manager.AddEntry(new LifetimeEntry { LifetimeStart = 0, LifetimeEnd = 2 });
162 manager.AddEntry(new LifetimeEntry { LifetimeStart = 1, LifetimeEnd = 2 });
163 manager.AddEntry(new LifetimeEntry { LifetimeStart = 2, LifetimeEnd = 2 });
164 manager.AddEntry(new LifetimeEntry { LifetimeStart = 2, LifetimeEnd = 3 });
165
166 checkCountAliveAt(-3, -2, 0);
167 checkCountAliveAt(-3, -1, 1);
168
169 checkCountAliveAt(-2, 4, 6);
170 checkCountAliveAt(-1, 4, 6);
171 checkCountAliveAt(0, 4, 6);
172 checkCountAliveAt(1, 4, 4);
173 checkCountAliveAt(2, 4, 1);
174 checkCountAliveAt(3, 4, 0);
175 checkCountAliveAt(4, 4, 0);
176 }
177
178 [Test]
179 public void TestRemoveFutureAfterLifetimeChange()
180 {
181 manager.AddEntry(new LifetimeEntry { LifetimeStart = 1, LifetimeEnd = 2 });
182 checkCountAliveAt(0, 0);
183
184 manager.Entries[0].LifetimeEnd = 3;
185 manager.RemoveEntry(manager.Entries[0]);
186
187 checkCountAliveAt(1, 0);
188 }
189
190 [Test]
191 public void TestRemovePastAfterLifetimeChange()
192 {
193 manager.AddEntry(new LifetimeEntry { LifetimeStart = -2, LifetimeEnd = -1 });
194 checkCountAliveAt(0, 0);
195
196 manager.Entries[0].LifetimeStart = -3;
197 manager.RemoveEntry(manager.Entries[0]);
198
199 checkCountAliveAt(-2, 0);
200 }
201
202 [Test]
203 public void TestFuzz()
204 {
205 var rng = new Random(2222);
206 int currentTime = 0;
207
208 addEntry();
209
210 manager.EntryCrossedBoundary += (entry, kind, direction) => changeLifetime();
211 manager.Update(0);
212
213 int count = 1;
214
215 for (int i = 0; i < 1000; i++)
216 {
217 switch (rng.Next(3))
218 {
219 case 0:
220 if (count < 20)
221 {
222 addEntry();
223 count += 1;
224 }
225 else
226 {
227 removeEntry();
228 count -= 1;
229 }
230
231 break;
232
233 case 1:
234 changeLifetime();
235 break;
236
237 case 2:
238 changeTime();
239 break;
240 }
241 }
242
243 void randomLifetime(out double l, out double r)
244 {
245 l = rng.Next(5);
246 r = rng.Next(5);
247
248 if (l > r)
249 (l, r) = (r, l);
250
251 ++r;
252 }
253
254 // ReSharper disable once AccessToModifiedClosure - intentional.
255 void checkAll() => checkAlivenessAt(currentTime);
256
257 void addEntry()
258 {
259 randomLifetime(out var l, out var r);
260 manager.AddEntry(new LifetimeEntry { LifetimeStart = l, LifetimeEnd = r });
261 checkAll();
262 }
263
264 void removeEntry()
265 {
266 var entry = manager.Entries[rng.Next(manager.Entries.Count)];
267 manager.RemoveEntry(entry);
268 checkAll();
269 }
270
271 void changeLifetime()
272 {
273 var entry = manager.Entries[rng.Next(manager.Entries.Count)];
274 randomLifetime(out var l, out var r);
275 entry.LifetimeStart = l;
276 entry.LifetimeEnd = r;
277 checkAll();
278 }
279
280 void changeTime()
281 {
282 int time = rng.Next(6);
283 currentTime = time;
284 checkAll();
285 }
286 }
287
288 private void checkCountAliveAt(int time, int expectedCount) => checkCountAliveAt(time, time, expectedCount);
289
290 private void checkCountAliveAt(int startTime, int endTime, int expectedCount)
291 {
292 checkAlivenessAt(startTime, endTime);
293 Assert.That(manager.Entries.Count(entry => entry.State == LifetimeEntryState.Current), Is.EqualTo(expectedCount));
294 }
295
296 private void checkAlivenessAt(int time) => checkAlivenessAt(time, time);
297
298 private void checkAlivenessAt(int startTime, int endTime)
299 {
300 manager.Update(startTime, endTime);
301
302 for (int i = 0; i < manager.Entries.Count; i++)
303 {
304 var entry = manager.Entries[i];
305 bool isAlive = entry.State == LifetimeEntryState.Current;
306 bool shouldBeAlive = endTime >= entry.LifetimeStart && startTime < entry.LifetimeEnd;
307
308 Assert.That(isAlive, Is.EqualTo(shouldBeAlive), $"Aliveness is invalid for entry {i}");
309 }
310 }
311
312 private void checkNoCrossing(LifetimeEntry entry) => Assert.That(manager.Crossings, Does.Not.Contain(entry));
313
314 private void checkCrossing(LifetimeEntry entry, int index, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
315 => Assert.That(manager.Crossings[entry][index], Is.EqualTo((kind, direction)));
316
317 private class TestLifetimeEntryManager : LifetimeEntryManager
318 {
319 public IReadOnlyList<LifetimeEntry> Entries => entries;
320
321 private readonly List<LifetimeEntry> entries = new List<LifetimeEntry>();
322
323 public IReadOnlyDictionary<LifetimeEntry, List<(LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)>> Crossings => crossings;
324
325 private readonly Dictionary<LifetimeEntry, List<(LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)>> crossings =
326 new Dictionary<LifetimeEntry, List<(LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)>>();
327
328 public TestLifetimeEntryManager()
329 {
330 EntryCrossedBoundary += (entry, kind, direction) =>
331 {
332 if (!crossings.ContainsKey(entry))
333 crossings[entry] = new List<(LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)>();
334 crossings[entry].Add((kind, direction));
335 };
336 }
337
338 public new void AddEntry(LifetimeEntry entry)
339 {
340 entries.Add(entry);
341 base.AddEntry(entry);
342 }
343
344 public new bool RemoveEntry(LifetimeEntry entry)
345 {
346 if (base.RemoveEntry(entry))
347 {
348 entries.Remove(entry);
349 return true;
350 }
351
352 return false;
353 }
354
355 public new void ClearEntries()
356 {
357 entries.Clear();
358 base.ClearEntries();
359 }
360
361 public new bool Update(double time)
362 {
363 crossings.Clear();
364 return base.Update(time);
365 }
366 }
367 }
368}