1 // 2 // Copyright (c) 2010-2022 Antmicro 3 // 4 // This file is licensed under the MIT License. 5 // Full license text is available in 'licenses/MIT.txt'. 6 // 7 using System; 8 using System.Collections.Generic; 9 using System.Threading; 10 using NUnit.Framework; 11 12 namespace Antmicro.Renode.UnitTests.Utilities 13 { 14 public class ThreadSyncTester : IDisposable 15 { ThreadSyncTester()16 public ThreadSyncTester() 17 { 18 errors = new List<string>(); 19 threads = new List<TestThread>(); 20 LocalThread = new TestThread("local"); 21 } 22 Dispose()23 public void Dispose() 24 { 25 foreach(var t in threads) 26 { 27 t.Dispose(); 28 } 29 LocalThread.Dispose(); 30 } 31 ReportError(string errorString)32 public void ReportError(string errorString) 33 { 34 errors.Add(errorString); 35 } 36 ObtainThread(string name)37 public TestThread ObtainThread(string name) 38 { 39 var t = new TestThread(name); 40 threads.Add(t); 41 return t; 42 } 43 Execute(TestThread t, Func<object> fun, string name = R)44 public ExecutionResult Execute(TestThread t, Func<object> fun, string name = "unnamed operation") 45 { 46 var result = new ExecutionResult(this, name); 47 var action = Tuple.Create(t, new DelayedAction(fun, result, name)); 48 action.Item2.ExecuteOn(action.Item1); 49 LocalThread.Wait(); 50 return result; 51 } 52 Finish()53 public void Finish() 54 { 55 ExecutionFinished = true; 56 foreach(var t in threads) 57 { 58 t.CheckException(); 59 } 60 LocalThread.Finish(); 61 62 if(errors.Count > 0) 63 { 64 Assert.Fail("Got errors:\n" + string.Join("\n", errors)); 65 } 66 } 67 68 public bool ExecutionFinished { get; private set; } 69 70 public TestThread LocalThread { get; private set; } 71 72 private readonly List<TestThread> threads; 73 private readonly List<string> errors; 74 75 public class TestThread : IDisposable 76 { TestThread(string name)77 public TestThread(string name) 78 { 79 Name = name; 80 pump = new AutoResetEvent(false); 81 report = new AutoResetEvent(false); 82 underlyingThread = new System.Threading.Thread(ThreadBody) 83 { 84 Name = name 85 }; 86 underlyingThread.Start(); 87 } 88 Dispose()89 public void Dispose() 90 { 91 #if NET 92 underlyingThread.Interrupt(); 93 #else 94 underlyingThread.Abort(); 95 #endif 96 underlyingThread.Join(); 97 } 98 Execute(Action a)99 public bool Execute(Action a) 100 { 101 if(CaughtException != null) 102 { 103 return false; 104 } 105 actionToRun = a; 106 pump.Set(); 107 report.WaitOne(); 108 return CaughtException == null; 109 } 110 CheckException()111 public void CheckException() 112 { 113 if(CaughtException != null) 114 { 115 throw CaughtException; 116 } 117 } 118 Finish()119 public void Finish() 120 { 121 Execute(null); 122 underlyingThread.Join(); 123 CheckException(); 124 } 125 Wait()126 public void Wait() 127 { 128 var mre = new ManualResetEvent(false); 129 if(Execute(() => mre.Set())) 130 { 131 mre.WaitOne(); 132 } 133 } 134 135 public string Name { get; private set; } 136 137 public Exception CaughtException { get; private set; } 138 ThreadBody()139 private void ThreadBody() 140 { 141 try 142 { 143 while(true) 144 { 145 pump.WaitOne(); 146 var atr = actionToRun; 147 report.Set(); 148 if(atr == null) 149 { 150 break; 151 } 152 atr(); 153 } 154 } 155 catch(Exception e) 156 { 157 // stop the thread on abort 158 CaughtException = e; 159 } 160 report.Set(); 161 } 162 163 private Action actionToRun; 164 private readonly System.Threading.Thread underlyingThread; 165 private readonly AutoResetEvent pump; 166 private readonly AutoResetEvent report; 167 } 168 169 public class DelayedAction 170 { DelayedAction(Func<object> a, ExecutionResult r, string name)171 public DelayedAction(Func<object> a, ExecutionResult r, string name) 172 { 173 fun = a; 174 executionResult = r; 175 Name = name; 176 } 177 ExecuteOn(TestThread t)178 public void ExecuteOn(TestThread t) 179 { 180 t.Execute(() => { 181 executionResult.Result = fun(); 182 executionResult.MarkAsFinished(); 183 }); 184 } 185 186 public string Name { get; private set; } 187 188 private readonly Func<object> fun; 189 private readonly ExecutionResult executionResult; 190 } 191 192 public class ExecutionResult 193 { ExecutionResult(ThreadSyncTester tester, string name)194 public ExecutionResult(ThreadSyncTester tester, string name) 195 { 196 this.tester = tester; 197 this.name = name; 198 actionFinished = new ManualResetEvent(false); 199 } 200 MarkAsFinished()201 public void MarkAsFinished() 202 { 203 actionFinished.Set(); 204 } 205 ShouldFinish(object result = null)206 public ExecutionResult ShouldFinish(object result = null) 207 { 208 tester.Execute(tester.LocalThread, () => { 209 if(!actionFinished.WaitOne(BlockingThreshold)) 210 { 211 tester.ReportError($"Expected operation '{name}' to finish, but it looks like being stuck."); 212 } 213 if(result != null) 214 { 215 if(!result.Equals(Result)) 216 { 217 tester.ReportError($"Expected {result} result of operation '{name}', but got {Result}"); 218 } 219 } 220 return null; 221 }, $"{name}: should finish"); 222 return this; 223 } 224 ShouldBlock()225 public ExecutionResult ShouldBlock() 226 { 227 tester.Execute(tester.LocalThread, () => { 228 if(actionFinished.WaitOne(BlockingThreshold)) 229 { 230 tester.ReportError($"Expected operation '{name}' to block, but it finished with result: {Result}."); 231 } 232 return null; 233 }, $"{name}: should block"); 234 return this; 235 } 236 237 public object Result 238 { 239 get; set; 240 } 241 242 private string name; 243 private readonly ManualResetEvent actionFinished; 244 private readonly ThreadSyncTester tester; 245 246 private const int BlockingThreshold = 5000; 247 } 248 } 249 }