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