A game framework written with osu! in mind.
at master 241 lines 7.5 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.Threading; 6using System.Threading.Tasks; 7using NUnit.Framework; 8using osu.Framework.Logging; 9using osu.Framework.Testing; 10 11namespace osu.Framework.Tests.IO 12{ 13 [TestFixture] 14 public class TestLogging 15 { 16 [Test] 17 public void TestExceptionLogging() 18 { 19 TestException resolvedException = null; 20 21 void logTest(LogEntry entry) 22 { 23 if (entry.Exception is TestException ex) 24 { 25 Assert.IsNull(resolvedException, "exception was forwarded more than once"); 26 resolvedException = ex; 27 } 28 } 29 30 using (var storage = new TemporaryNativeStorage(nameof(TestExceptionLogging))) 31 { 32 Logger.Storage = storage; 33 Logger.Enabled = true; 34 35 Logger.NewEntry += logTest; 36 Logger.Error(new TestException(), "message"); 37 Logger.NewEntry -= logTest; 38 39 Assert.IsNotNull(resolvedException, "exception wasn't forwarded by logger"); 40 41 Logger.Enabled = false; 42 Logger.Flush(); 43 } 44 } 45 46 [Test] 47 public void TestUnhandledExceptionLogging() 48 { 49 TestException resolvedException = null; 50 51 void logTest(LogEntry entry) 52 { 53 if (entry.Exception is TestException ex) 54 { 55 Assert.IsNull(resolvedException, "exception was forwarded more than once"); 56 resolvedException = ex; 57 } 58 } 59 60 Logger.NewEntry += logTest; 61 62 try 63 { 64 using (var host = new TestRunHeadlessGameHost()) 65 { 66 var game = new TestGame(); 67 game.Schedule(() => throw new TestException()); 68 host.Run(game); 69 } 70 } 71 catch 72 { 73 // catch crashing exception 74 } 75 76 Assert.IsNotNull(resolvedException, "exception wasn't forwarded by logger"); 77 Logger.NewEntry -= logTest; 78 } 79 80 [Test] 81 public void TestUnhandledIgnoredException() 82 { 83 Assert.DoesNotThrow(() => runWithIgnoreCount(2, 2)); 84 } 85 86 [Test] 87 public void TestUnhandledIgnoredOnceException() 88 { 89 Assert.Throws<TestException>(() => runWithIgnoreCount(1, 2)); 90 } 91 92 /// <summary> 93 /// Ignore unhandled exceptions for the provided count. 94 /// </summary> 95 /// <param name="ignoreCount">Number of exceptions to ignore.</param> 96 /// <param name="fireCount">How many exceptions to fire.</param> 97 private void runWithIgnoreCount(int ignoreCount, int fireCount) 98 { 99 using (var host = new TestRunHeadlessGameHost()) 100 { 101 host.ExceptionThrown += ex => ignoreCount-- > 0; 102 103 var game = new TestGame(); 104 105 for (int i = 0; i < fireCount; i++) 106 game.Schedule(() => throw new TestException()); 107 game.Schedule(() => game.Exit()); 108 109 host.Run(game); 110 } 111 } 112 113 [Test] 114 public void TestGameUpdateExceptionNoLogging() 115 { 116 Assert.Throws<TestException>(() => 117 { 118 using (var host = new TestRunHeadlessGameHost()) 119 host.Run(new CrashTestGame()); 120 }); 121 } 122 123 private class CrashTestGame : Game 124 { 125 protected override void Update() 126 { 127 base.Update(); 128 throw new TestException(); 129 } 130 } 131 132 [Test] 133 public void TestGameUnobservedExceptionDoesntCrashGame() 134 { 135 using (var host = new TestRunHeadlessGameHost()) 136 { 137 TaskCrashTestGame game = new TaskCrashTestGame(); 138 host.Run(game); 139 } 140 } 141 142 private class TaskCrashTestGame : Game 143 { 144 private int frameCount; 145 146 protected override void Update() 147 { 148 base.Update(); 149 150 Task.Run(() => throw new TestException()); 151 152 // only start counting frames once the task has completed, to allow some time for the unobserved exception to be handled. 153 if (frameCount++ > 10) 154 Exit(); 155 } 156 } 157 158 [Test] 159 public void TestTaskExceptionLogging() 160 { 161 Exception resolvedException = null; 162 163 void logTest(LogEntry entry) 164 { 165 if (entry.Exception is AggregateException ex) 166 { 167 Assert.IsNull(resolvedException, "exception was forwarded more than once"); 168 resolvedException = ex; 169 } 170 } 171 172 Logger.NewEntry += logTest; 173 174 using (new BackgroundGameHeadlessGameHost()) 175 { 176 // see https://tpodolak.com/blog/2015/08/10/tpl-exception-handling-and-unobservedtaskexception-issue/ 177 // needs to be in a separate method so the Task gets GC'd. 178 performTaskException(); 179 180 GC.Collect(); 181 GC.WaitForPendingFinalizers(); 182 } 183 184 Assert.IsNotNull(resolvedException, "exception wasn't forwarded by logger"); 185 Logger.NewEntry -= logTest; 186 } 187 188 private void performTaskException() 189 { 190 var task = Task.Run(() => throw new TestException()); 191 while (!task.IsCompleted) 192 Thread.Sleep(1); 193 } 194 195 [Test] 196 public void TestRecursiveExceptionLogging() 197 { 198 TestExceptionWithInnerException resolvedException = null; 199 TestInnerException resolvedInnerException = null; 200 201 void logTest(LogEntry entry) 202 { 203 if (entry.Exception is TestExceptionWithInnerException ex) 204 { 205 Assert.IsNull(resolvedException, "exception was forwarded more than once"); 206 resolvedException = ex; 207 } 208 209 if (entry.Exception is TestInnerException inner) 210 { 211 Assert.IsNull(resolvedInnerException, "exception was forwarded more than once"); 212 resolvedInnerException = inner; 213 } 214 } 215 216 Logger.Enabled = true; 217 Logger.NewEntry += logTest; 218 Logger.Error(new TestExceptionWithInnerException(), "message", recursive: true); 219 Logger.NewEntry -= logTest; 220 221 Assert.IsNotNull(resolvedException, "exception wasn't forwarded by logger"); 222 Assert.IsNotNull(resolvedInnerException, "inner exception wasn't forwarded by logger"); 223 } 224 225 private class TestException : Exception 226 { 227 } 228 229 public class TestExceptionWithInnerException : Exception 230 { 231 public TestExceptionWithInnerException() 232 : base("", new TestInnerException()) 233 { 234 } 235 } 236 237 private class TestInnerException : Exception 238 { 239 } 240 } 241}