1 //
2 // Copyright (c) 2010-2023 Antmicro
3 // Copyright (c) 2011-2015 Realtime Embedded
4 //
5 // This file is licensed under the MIT License.
6 // Full license text is available in 'licenses/MIT.txt'.
7 //
8 using System;
9 using Antmicro.Renode.Core;
10 using Antmicro.Renode.Peripherals;
11 using System.Threading;
12 using Antmicro.Renode.Peripherals.Miscellaneous;
13 using Antmicro.Renode.Utilities;
14 using Antmicro.Renode.Time;
15 using System.Runtime.CompilerServices;
16 
17 namespace Antmicro.Renode.Testing
18 {
19     public static class LEDTesterExtenions
20     {
CreateLEDTester(this Emulation emulation, string name, ILed led, float defaultTimeout = 0)21         public static void CreateLEDTester(this Emulation emulation, string name, ILed led, float defaultTimeout = 0)
22         {
23             emulation.ExternalsManager.AddExternal(new LEDTester(led, defaultTimeout), name);
24         }
25     }
26 
27     public class LEDTester : IExternal
28     {
LEDTester(ILed led, float defaultTimeout = 0)29         public LEDTester(ILed led, float defaultTimeout = 0)
30         {
31             ValidateArgument(defaultTimeout, nameof(defaultTimeout), allowZero: true);
32             this.led = led;
33             this.machine = led.GetMachine();
34             this.defaultTimeout = defaultTimeout;
35         }
36 
AssertState(bool state, float? timeout = null, bool pauseEmulation = false)37         public LEDTester AssertState(bool state, float? timeout = null, bool pauseEmulation = false)
38         {
39             timeout = timeout ?? defaultTimeout;
40             ValidateArgument(timeout.Value, nameof(timeout), allowZero: true);
41 
42             var emulation = EmulationManager.Instance.CurrentEmulation;
43             var timeoutEvent = GetTimeoutEvent((ulong)(timeout * 1000));
44             AutoResetEvent emulationPausedEvent = null;
45 
46             var ev = new ManualResetEvent(false);
47             var method = (Action<ILed, bool>)
48             ((led, currState) =>
49             {
50                 if(pauseEmulation && currState == state)
51                 {
52                     machine.PauseAndRequestEmulationPause(precise: true);
53                 }
54                 ev.Set();
55             });
56 
57             try
58             {
59                 led.StateChanged += method;
60                 // Don't start the emulation if the assert would succeed instantly
61                 // or regardless of the LED state if the timeout is 0
62                 if(led.State != state && timeout != 0)
63                 {
64                     emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation);
65                 }
66 
67                 do
68                 {
69                     if(led.State == state)
70                     {
71                         emulationPausedEvent?.WaitOne();
72                         return this;
73                     }
74 
75                     WaitHandle.WaitAny(new [] { timeoutEvent.WaitHandle, ev });
76                 }
77                 while(!timeoutEvent.IsTriggered);
78             }
79             finally
80             {
81                 led.StateChanged -= method;
82             }
83 
84             throw new InvalidOperationException("LED assertion not met.");
85         }
86 
AssertAndHoldState(bool initialState, float timeoutAssert, float timeoutHold, bool pauseEmulation = false)87         public LEDTester AssertAndHoldState(bool initialState, float timeoutAssert, float timeoutHold, bool pauseEmulation = false)
88         {
89             ValidateArgument(timeoutAssert, nameof(timeoutAssert), allowZero: true);
90             ValidateArgument(timeoutHold, nameof(timeoutHold));
91             var emulation = EmulationManager.Instance.CurrentEmulation;
92             AutoResetEvent emulationPausedEvent = null;
93             var locker = new Object();
94             int numberOfStateChanges = 0;
95             bool isHolding = false;
96 
97             var ev = new AutoResetEvent(false);
98             TimeoutEvent timeoutEvent;
99             var method = (Action<ILed, bool>)
100             ((led, currState) =>
101             {
102                 lock(locker)
103                 {
104                     ++numberOfStateChanges;
105                     if(!isHolding && numberOfStateChanges == 1)
106                     {
107                         // Create a new event for holding at the precise moment that the LED state changed
108                         timeoutEvent = GetTimeoutEvent((ulong)(timeoutHold * 1000), MakePauseRequest(emulation, pauseEmulation));
109                     }
110                     ev?.Set();
111                 }
112             });
113 
114             try
115             {
116                 // this needs to be treated as atomic block so the state doesn't change during initialization
117                 lock(locker)
118                 {
119                     led.StateChanged += method;
120                     isHolding = initialState == led.State;
121                     var timeout = isHolding ? timeoutHold : timeoutAssert;
122                     // If we're already holding, make the first timeout event pause the emulation
123                     timeoutEvent = GetTimeoutEvent((ulong)(timeout * 1000),
124                         MakePauseRequest(emulation, pauseEmulation && isHolding));
125                     emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation);
126                 }
127 
128                 do
129                 {
130                     var eventSrc = WaitHandle.WaitAny( new [] { timeoutEvent.WaitHandle, ev } );
131 
132                     if(isHolding)
133                     {
134                         if(numberOfStateChanges > 0)
135                         {
136                             throw new InvalidOperationException("LED changed state.");
137                         }
138                     }
139                     else
140                     {
141                         lock(locker)
142                         {
143                             if(numberOfStateChanges == 1)
144                             {
145                                 isHolding = true;
146                                 --numberOfStateChanges;
147                             }
148                             else if(eventSrc == 0)
149                             {
150                                 throw new InvalidOperationException("Initial LED assertion not met.");
151                             }
152                             else
153                             {
154                                 throw new InvalidOperationException("LED changed state.");
155                             }
156                         }
157                     }
158                 }
159                 while((!timeoutEvent.IsTriggered && isHolding) || !isHolding);
160             }
161             finally
162             {
163                 lock(locker)
164                 {
165                     led.StateChanged -= method;
166                     ev.Dispose();
167                     ev = null;
168                 }
169             }
170 
171             emulationPausedEvent?.WaitOne();
172 
173             return this;
174         }
175 
AssertDutyCycle(float testDuration, double expectedDutyCycle, double tolerance = 0.05, bool pauseEmulation = false)176         public LEDTester AssertDutyCycle(float testDuration, double expectedDutyCycle, double tolerance = 0.05, bool pauseEmulation = false)
177         {
178             ValidateArgument(testDuration, nameof(testDuration));
179             ValidateArgument(expectedDutyCycle, nameof(expectedDutyCycle), min: 0, max: 1);
180             ValidateArgument(tolerance, nameof(tolerance), min: 0, max: 1);
181             var emulation = EmulationManager.Instance.CurrentEmulation;
182             AutoResetEvent emulationPausedEvent = null;
183             ulong lowTicks = 0;
184             ulong highTicks = 0;
185 
186             var method = MakeStateChangeHandler((currState, dt) =>
187             {
188                 if(currState)
189                 {
190                     // we switch to high, so up to this point it was low
191                     lowTicks += dt.Ticks;
192                 }
193                 else
194                 {
195                     highTicks += dt.Ticks;
196                 }
197             });
198 
199             try
200             {
201                 led.StateChanged += method;
202                 var timeoutEvent = GetTimeoutEvent((ulong)(testDuration * 1000), MakePauseRequest(emulation, pauseEmulation));
203                 emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation);
204 
205                 timeoutEvent.WaitHandle.WaitOne();
206 
207                 var highPercentage = (double)highTicks / (highTicks + lowTicks) * 100;
208                 if(highPercentage < expectedDutyCycle - (tolerance * 100) || expectedDutyCycle > expectedDutyCycle + (tolerance * 100))
209                 {
210                     throw new InvalidOperationException($"Fill assertion not met: expected {expectedDutyCycle} with tolerance {tolerance * 100}%, but got {highPercentage}");
211                 }
212             }
213             finally
214             {
215                 led.StateChanged -= method;
216             }
217 
218             emulationPausedEvent?.WaitOne();
219 
220             return this;
221         }
222 
AssertIsBlinking(float testDuration, double onDuration, double offDuration, double tolerance = 0.05, bool pauseEmulation = false)223         public LEDTester AssertIsBlinking(float testDuration, double onDuration, double offDuration, double tolerance = 0.05, bool pauseEmulation = false)
224         {
225             ValidateArgument(testDuration, nameof(testDuration));
226             ValidateArgument(onDuration, nameof(onDuration));
227             ValidateArgument(offDuration, nameof(offDuration));
228             var emulation = EmulationManager.Instance.CurrentEmulation;
229             AutoResetEvent emulationPausedEvent = null;
230             var stateChanged = false;
231             var patternMismatchEvent = new ManualResetEvent(false);
232             var method = MakeStateChangeHandler((currState, dt) =>
233             {
234                 stateChanged = true;
235                 // currState is after a switch, so when it's high we need to check the off duration
236                 var expectedDuration = currState ? offDuration : onDuration;
237                 if(!IsInRange(dt.TotalSeconds, expectedDuration, tolerance))
238                 {
239                     patternMismatchEvent.Set();
240                 }
241             });
242 
243             try
244             {
245                 led.StateChanged += method;
246                 var timeoutEvent = GetTimeoutEvent((ulong)(testDuration * 1000), MakePauseRequest(emulation, pauseEmulation));
247                 emulationPausedEvent = StartEmulationAndGetPausedEvent(emulation, pauseEmulation);
248                 var eventIdx = WaitHandle.WaitAny( new [] { timeoutEvent.WaitHandle, patternMismatchEvent } );
249 
250                 if(!stateChanged)
251                 {
252                     throw new InvalidOperationException("Expected blinking pattern not detected (LED state never changed)");
253                 }
254                 if(eventIdx == 1)
255                 {
256                     throw new InvalidOperationException("Expected blinking pattern not detected (State duration was out of specified range)");
257                 }
258             }
259             finally
260             {
261                 led.StateChanged -= method;
262             }
263 
264             emulationPausedEvent?.WaitOne();
265 
266             return this;
267         }
268 
269         [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
IsInRange(double actualValue, double expectedValue, double tolerance)270         private bool IsInRange(double actualValue, double expectedValue, double tolerance)
271         {
272             return (actualValue >= expectedValue * (1 - tolerance)) && (actualValue <= (expectedValue * (1 + tolerance)));
273         }
274 
275         [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
GetTimeoutEvent(ulong timeout, Action callback = null)276         private TimeoutEvent GetTimeoutEvent(ulong timeout, Action callback = null)
277         {
278             return machine.LocalTimeSource.EnqueueTimeoutEvent(timeout, callback);
279         }
280 
MakeStateChangeHandler(Action<bool, TimeInterval> stateChanged)281         private Action<ILed, bool> MakeStateChangeHandler(Action<bool, TimeInterval> stateChanged)
282         {
283             TimeStamp? previousEventTimestamp = null;
284             return (Action<ILed, bool>)((led, currState) =>
285             {
286                 if(!TimeDomainsManager.Instance.TryGetVirtualTimeStamp(out var vts))
287                 {
288                     throw new InvalidOperationException("Couldn't obtain virtual time");
289                 }
290 
291                 // first we need to "sync" with the first state change;
292                 // it doesn't matter if that's low/high or high/low transition
293                 if(previousEventTimestamp == null)
294                 {
295                     previousEventTimestamp = vts;
296                     return;
297                 }
298 
299                 TimeInterval dt;
300                 // TODO: Below `if` block is a way to avoid TimeInterval underflow and the resulting exception
301                 // that occurs when the OnGPIO comes from a different thread(core) than expected.
302                 // This should be fixed by making sure we always get the Timestamp from the correct thread.
303                 // Until then the results will be radomly less accurate.
304                 if(vts.TimeElapsed > previousEventTimestamp.Value.TimeElapsed)
305                 {
306                     dt = vts.TimeElapsed - previousEventTimestamp.Value.TimeElapsed;
307                 }
308                 else
309                 {
310                     dt = TimeInterval.Empty;
311                 }
312 
313                 stateChanged.Invoke(currState, dt);
314 
315                 previousEventTimestamp = vts;
316             });
317         }
318 
MakePauseRequest(Emulation emulation, bool pause)319         private Action MakePauseRequest(Emulation emulation, bool pause)
320         {
321             return pause ? (Action)(() =>
322             {
323                 emulation.PauseAll();
324             }) : null;
325         }
326 
StartEmulationAndGetPausedEvent(Emulation emulation, bool pause)327         private AutoResetEvent StartEmulationAndGetPausedEvent(Emulation emulation, bool pause)
328         {
329             var emulationPausedEvent = pause ? emulation.GetStartedStateChangedEvent(false) : null;
330             if(!emulation.IsStarted)
331             {
332                 emulation.StartAll();
333             }
334             return emulationPausedEvent;
335         }
336 
337         // If no min or max is provided, this function checks whether the argument is not negative
338         // (if allowZero) or positive (otherwise)
ValidateArgument(double value, string name, bool allowZero = false, double? min = null, double? max = null)339         private static void ValidateArgument(double value, string name, bool allowZero = false,
340             double? min = null, double? max = null)
341         {
342             if(min != null || max != null)
343             {
344                 if(value < min || value > max)
345                 {
346                     throw new ArgumentException($"Value must be in range [{min}; {max}], but was {value}", name);
347                 }
348             }
349             else if(value < 0 || !allowZero && value == 0)
350             {
351                 var explanation = allowZero ? "not be negative" : "be positive";
352                 throw new ArgumentException($"Value must {explanation}, but was {value}", name);
353             }
354         }
355 
356         private readonly ILed led;
357         private readonly IMachine machine;
358         private readonly float defaultTimeout;
359     }
360 }
361 
362