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 }