CrabUI
Loading...
Searching...
No Matches
UnitTest.cs
1using System;
2using System.Reflection;
3using System.Collections.Generic;
4using System.Linq;
5
6using Barotrauma;
7using HarmonyLib;
8using Microsoft.Xna.Framework;
9
10
11namespace CrabUITest
12{
13 #region UnitTest
14
15 public class TestForAttribute : System.Attribute
16 {
17 public string Name;
18 public TestForAttribute(string name)
19 {
20 Name = name;
21 }
22 }
23
24 public class UnitTest
25 {
26 public class TestContext
27 {
28 public string Description = "";
29
30 public TestContext(string description = "")
31 {
32 Description = description;
33 }
34
35 public override string ToString() => Description;
36
37 public static TestContext operator +(TestContext a, TestContext b)
38 => new TestContext(a.Description + " | " + b.Description);
39 }
40
41 public class TestResult
42 {
43 public TestContext Context;
44 public object Result;
45 public bool? State;
46 public bool Error;
47 public Exception exception;
48
49 public void ToBeEqual(object o) => State = Object.Equals(Result, o);
50 public void ToBeNotEqual(object o) => State = !Object.Equals(Result, o);
51 public void ToThrow() => State = Error;
52 public void ToNotThrow() => State = !Error;
53 }
54
55 public static string GetTestClassName(string name) => name + "Test";
56 public static string GetTestMethodName(string name) => name + "Test";
57 public static bool IsTestable(MemberInfo member, Type original)
58 {
59 if (member is MethodInfo method)
60 {
61 if (
62 method.IsSpecialName ||
63 // method.IsConstructor ||
64 method.DeclaringType != original
65 ) return false;
66
67 return true;
68 }
69
70 return false;
71 }
72
73 public static bool IsTestingMethod(MethodInfo mi) => mi.Name.EndsWith("Test") || Attribute.IsDefined(mi, typeof(TestForAttribute));
74
75 public static Dictionary<string, Type> FindAllNonTests()
76 {
77 Assembly TestAssembly = Assembly.GetAssembly(typeof(UnitTest));
78
79 Dictionary<string, Type> types = new();
80
81 foreach (Type t in TestAssembly.GetTypes().Where(t => !t.IsSubclassOf(typeof(UnitTest))))
82 {
83 if (t.IsSpecialName) continue;
84 if (t.Name.StartsWith("<>")) continue;
85 if (t == typeof(UnitTest)) continue;
86 if (t == typeof(TestForAttribute)) continue;
87 if (t == typeof(TestContext)) continue;
88 if (t == typeof(TestResult)) continue;
89 if (t.IsAssignableTo(typeof(IAssemblyPlugin))) continue;
90
91 types[t.Name] = t;
92 }
93
94 return types;
95 }
96
97 public static Dictionary<string, Type> FindAllTests()
98 {
99 Assembly TestAssembly = Assembly.GetAssembly(typeof(UnitTest));
100
101 Dictionary<string, Type> testClasses = new();
102
103 foreach (Type t in TestAssembly.GetTypes().Where(t => t.IsSubclassOf(typeof(UnitTest))))
104 {
105 TestForAttribute attribute = t.GetCustomAttribute<TestForAttribute>();
106 if (attribute != null)
107 {
108 testClasses[GetTestClassName(attribute.Name)] = t;
109 }
110 else
111 {
112 testClasses[t.Name] = t;
113 }
114 }
115
116 return testClasses;
117 }
118
119 public static Dictionary<string, MethodInfo> FindAllTestMethods(Type testType)
120 {
121 Dictionary<string, MethodInfo> testMethods = new();
122
123 foreach (MethodInfo mi in testType.GetMethods(AccessTools.all))
124 {
125 if (IsTestingMethod(mi))
126 {
127 TestForAttribute attribute = mi.GetCustomAttribute<TestForAttribute>();
128 if (attribute != null)
129 {
130 testMethods[GetTestMethodName(attribute.Name)] = mi;
131 }
132 else
133 {
134 testMethods[mi.Name] = mi;
135 }
136 }
137 }
138
139 return testMethods;
140 }
141
142
143
144 public static void Coverage(IEnumerable<Type> originalTypes)
145 {
146 Dictionary<string, Type> AllTests = FindAllTests();
147
148 foreach (Type original in originalTypes)
149 {
150 if (original == null) continue;
151
152 Type testType = AllTests.GetValueOrDefault(GetTestClassName(original.Name));
153 if (testType == null)
154 {
155 Log($"No test class for type {original}");
156 }
157 else
158 {
159 Coverage(original, testType);
160 }
161 }
162 }
163
164 public static void Coverage(string typeName)
165 {
166 if (typeName == null || typeName == "") { Log($"for what?"); return; };
167
168 List<Type> TestAssemblyTypes = Assembly.GetAssembly(typeof(UnitTest)).GetTypes().ToList();
169
170 Type original = null;
171
172 original ??= TestAssemblyTypes.Find(T => T.FullName.Equals(typeName, StringComparison.OrdinalIgnoreCase));
173 original ??= TestAssemblyTypes.Find(T => T.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));
174
175 if (original != null)
176 {
177 Coverage(original);
178 return;
179 }
180
181 List<Type> BaroAssemblyTypes = Assembly.GetAssembly(typeof(GameMain)).GetTypes().ToList();
182
183 original ??= BaroAssemblyTypes.Find(T => T.FullName.Equals(typeName, StringComparison.OrdinalIgnoreCase));
184 original ??= BaroAssemblyTypes.Find(T => T.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));
185
186 Coverage(original);
187 }
188
189 public static void Coverage(Type original)
190 {
191 if (original == null) { Log($"No such type"); return; };
192
193 Dictionary<string, Type> AllTests = FindAllTests();
194
195 Type testType = AllTests.GetValueOrDefault(GetTestClassName(original.Name));
196 if (testType == null)
197 {
198 Log($"No test class for type {original}");
199 }
200 else
201 {
202 Coverage(original, testType);
203 }
204 }
205
206 public static void Coverage(Type original, Type testType)
207 {
208 if (original == null) { Log($"original type is null"); return; }
209 if (testType == null) { Log($"test type is null"); return; }
210
211 MethodInfo customIsTestable = testType.GetMethod("IsTestable", AccessTools.all);
212 Dictionary<string, MethodInfo> testMethods = FindAllTestMethods(testType);
213
214 Log($"Coverage for {original.Name}:");
215 try
216 {
217 foreach (MemberInfo mi in original.GetMembers(AccessTools.all))
218 {
219 bool testable = true;
220
221 if (customIsTestable != null)
222 {
223 testable = Convert.ToBoolean(customIsTestable.Invoke(null, new object[] { mi, original }));
224 }
225 else
226 {
227 testable = IsTestable(mi, original);
228 }
229
230 if (!testable) continue;
231
232 bool covered = testMethods.ContainsKey(GetTestMethodName(mi.Name));
233
234 if (covered) Log($" {mi}", Color.Lime);
235 else Log($" {mi}", Color.Gray);
236 }
237 }
238 catch (Exception e) { Log(e, Color.Orange); }
239 }
240
241
242
243 public static void RunAll()
244 {
245 IEnumerable<Type> AllTests = FindAllTests().Values;
246 if (AllTests.Count() == 0) Log($"no tests");
247 foreach (Type T in AllTests) { Run(T); }
248 }
249
250 public static void Run(string name, string method = null)
251 {
252 Dictionary<string, Type> AllTests = FindAllTests();
253
254 if (String.Equals("all", name, StringComparison.OrdinalIgnoreCase))
255 {
256 if (AllTests.Count == 0)
257 {
258 Log($"no tests");
259 }
260 else
261 {
262 foreach (Type T in AllTests.Values) { Run(T); }
263 }
264
265 return;
266 }
267
268 if (AllTests.ContainsKey(name))
269 {
270 Run(AllTests[name], method);
271 return;
272 }
273
274 Log($"{name} not found");
275 }
276
277 public static void Run<RawType>(string method = null) => Run(typeof(RawType), method);
278 public static void Run(Type T, string method = null)
279 {
280 if (!T.IsSubclassOf(typeof(UnitTest)))
281 {
282 Log($"{T} is not a test!");
283 return;
284 }
285
286
287 Log($"------------------------");
288 Log($"Running {T}");
289 UnitTest test = (UnitTest)Activator.CreateInstance(T);
290
291 try
292 {
293 test.Prepare();
294 test.Execute(method);
295 }
296 catch (Exception e)
297 {
298 Log($"{T} Execution failed with:\n{e}", Color.Yellow);
299 }
300 finally
301 {
302 test.Finalize();
303 }
304
305 test.PrintResults();
306 }
307
308 public List<TestResult> Results = new List<TestResult>();
309 public TestContext Context = new TestContext();
310 public virtual void Prepare() { }
311
312 public virtual void Finalize() { }
313
314 public virtual void Execute(string method = null)
315 {
316 if (method == null)
317 {
318 IEnumerable<MethodInfo> methods = this.GetType().GetMethods().Where(mi => IsTestingMethod(mi));
319
320 foreach (MethodInfo mi in methods)
321 {
322 Describe(mi.Name, () => mi.Invoke(this, new object[] { }));
323 }
324 }
325 else
326 {
327 MethodInfo mi = this.GetType().GetMethod(method);
328 mi ??= this.GetType().GetMethod(GetTestMethodName(method));
329
330 if (mi != null)
331 {
332 Describe(mi.Name, () => mi.Invoke(this, new object[] { }));
333 }
334 else
335 {
336 Log($"method {method} in {this.GetType()} not found", Color.Orange);
337 }
338 }
339 }
340
341
342 public void Describe(string description, Action test)
343 {
344 TestContext oldContext = Context;
345 Context = oldContext + new TestContext(description);
346 test();
347 Context = oldContext;
348 }
349
350
351 public TestResult Expect(Action test, string context = null)
352 {
353 TestResult result = new TestResult();
354 if (context != null) result.Context = Context + new TestContext(context);
355 result.Context ??= Context;
356
357 try
358 {
359 test();
360 }
361 catch (Exception e)
362 {
363 result.Error = true;
364 result.exception = e;
365 }
366
367 Results.Add(result);
368 return result;
369 }
370 public TestResult Expect(Func<object> test, string context = null)
371 {
372 TestResult result = new TestResult();
373 if (context != null) result.Context = Context + new TestContext(context);
374 result.Context ??= Context;
375
376 try
377 {
378 result.Result = test();
379 }
380 catch (Exception e)
381 {
382 result.Error = true;
383 result.exception = e;
384 }
385
386 Results.Add(result);
387 return result;
388 }
389
390 public TestResult Expect(object o, string context = null)
391 {
392 TestResult result = new TestResult();
393 if (context != null) result.Context = Context + new TestContext(context);
394 result.Context ??= Context;
395
396 result.Result = o;
397
398 Results.Add(result);
399 return result;
400 }
401
402 public void PrintResults()
403 {
404 int passed = 0;
405 foreach (TestResult tr in Results)
406 {
407 if (tr.State.HasValue && tr.State.Value) passed++;
408
409 Color cl;
410 if (tr.State.HasValue)
411 {
412 cl = tr.State.Value ? Color.Lime : Color.Red;
413 }
414 else
415 {
416 cl = Color.White;
417 }
418
419 object result = tr.Error ? tr.exception.Message : tr.Result;
420
421 Log($"{tr.Context} [{result}]", cl);
422 }
423
424 string conclusion = passed == Results.Count ? "All Passed" : "Passed";
425
426 Log($"\n{passed}/{Results.Count} {conclusion}");
427 }
428
429 public UnitTest()
430 {
431 Context = new TestContext(this.GetType().Name);
432 }
433
434 public static void Log(object msg, Color? cl = null)
435 {
436 cl ??= Color.Cyan;
437 LuaCsLogger.LogMessage($"{msg ?? "null"}", cl * 0.8f, cl);
438 }
439 }
440
441 #endregion
442
443}