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