View Javadoc
1   package trafficcontrol;
2   
3   import static org.junit.jupiter.api.Assertions.assertEquals;
4   import static org.junit.jupiter.api.Assertions.assertTrue;
5   import static org.junit.jupiter.api.Assertions.fail;
6   
7   import java.io.ByteArrayOutputStream;
8   import java.io.PrintStream;
9   import java.util.LinkedHashMap;
10  import java.util.LinkedHashSet;
11  import java.util.Map;
12  import java.util.Set;
13  
14  import javax.naming.NamingException;
15  
16  import org.djunits.value.vdouble.scalar.Duration;
17  import org.djutils.immutablecollections.ImmutableSet;
18  import org.mockito.ArgumentMatchers;
19  import org.mockito.Mockito;
20  import org.mockito.invocation.InvocationOnMock;
21  import org.mockito.stubbing.Answer;
22  import org.opentrafficsim.core.dsol.OtsModelInterface;
23  import org.opentrafficsim.core.dsol.OtsSimulator;
24  import org.opentrafficsim.core.dsol.OtsSimulatorInterface;
25  import org.opentrafficsim.core.network.Network;
26  import org.opentrafficsim.core.network.NetworkException;
27  import org.opentrafficsim.core.perception.HistoryManagerDevs;
28  import org.opentrafficsim.road.network.object.trafficlight.TrafficLight;
29  import org.opentrafficsim.road.network.object.trafficlight.TrafficLightColor;
30  import org.opentrafficsim.trafficcontrol.FixedTimeController;
31  import org.opentrafficsim.trafficcontrol.FixedTimeController.SignalGroup;
32  
33  import nl.tudelft.simulation.dsol.SimRuntimeException;
34  
35  /**
36   * Test the fixed time traffic controller class.
37   * <p>
38   * Copyright (c) 2013-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
39   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
40   * </p>
41   * @author Alexander Verbraeck
42   * @author Peter Knoppers
43   * @author Wouter Schakel
44   */
45  public final class TestFixedTimeController
46  {
47  
48      /** */
49      private TestFixedTimeController()
50      {
51          // do not instantiate test class
52      }
53  
54      /**
55       * Test the constructors and initializers of the signal group and fixed time controller classes.
56       * @throws SimRuntimeException if that happens uncaught; this test has failed
57       * @throws NamingException if that happens uncaught; this test has failed
58       * @throws NetworkException on exception
59       */
60      // TODO: @Test
61      public void testConstructors() throws SimRuntimeException, NamingException, NetworkException
62      {
63          String signalGroupId = "sgId";
64          Set<String> trafficLightIds = new LinkedHashSet<>();
65          String trafficLightId = "08.1";
66          trafficLightIds.add(trafficLightId);
67          Duration signalGroupOffset = Duration.ofSI(5);
68          Duration preGreen = Duration.ofSI(2);
69          Duration green = Duration.ofSI(10);
70          Duration yellow = Duration.ofSI(3.5);
71          try
72          {
73              new SignalGroup(null, trafficLightIds, signalGroupOffset, preGreen, green, yellow);
74              fail("Null pointer for signalGroupId should have thrown a null pointer exception");
75          }
76          catch (NullPointerException npe)
77          {
78              // Ignore expected exception
79          }
80          try
81          {
82              new SignalGroup(signalGroupId, null, signalGroupOffset, preGreen, green, yellow);
83              fail("Null pointer for trafficLightIds should have thrown a null pointer exception");
84          }
85          catch (NullPointerException npe)
86          {
87              // Ignore expected exception
88          }
89          try
90          {
91              new SignalGroup(signalGroupId, trafficLightIds, null, preGreen, green, yellow);
92              fail("Null pointer for signalGroupOffset should have thrown a null pointer exception");
93          }
94          catch (NullPointerException npe)
95          {
96              // Ignore expected exception
97          }
98          try
99          {
100             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, null, green, yellow);
101             fail("Null pointer for preGreen should have thrown a null pointer exception");
102         }
103         catch (NullPointerException npe)
104         {
105             // Ignore expected exception
106         }
107         try
108         {
109             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, null, yellow);
110             fail("Null pointer for green should have thrown a null pointer exception");
111         }
112         catch (NullPointerException npe)
113         {
114             // Ignore expected exception
115         }
116         try
117         {
118             new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, green, null);
119             fail("Null pointer for yellow should have thrown a null pointer exception");
120         }
121         catch (NullPointerException npe)
122         {
123             // Ignore expected exception
124         }
125         try
126         {
127             new SignalGroup(signalGroupId, new LinkedHashSet<String>(), signalGroupOffset, preGreen, green, yellow);
128             fail("Empty list of traffic light ids should have thrown an illegal argument exception");
129         }
130         catch (IllegalArgumentException iae)
131         {
132             // Ignore expected exception
133         }
134         // Test the controller that adds default for pre green time
135         SignalGroup sg = new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, green, yellow);
136         assertEquals(0, sg.getPreGreen().si, 0, "default for pre green");
137         assertEquals(green.si, sg.getGreen().si, 0, "green");
138         assertEquals(yellow.si, sg.getYellow().si, 0, "yellow");
139         // Now that we've tested all ways that the constructor should have told us to go to hell, create a signal group
140         sg = new SignalGroup(signalGroupId, trafficLightIds, signalGroupOffset, preGreen, green, yellow);
141         assertEquals(signalGroupId, sg.getId(), "group id");
142         assertTrue(sg.toString().startsWith("SignalGroup ["), "toString returns something descriptive");
143 
144         String ftcId = "FTCid";
145         OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
146         simulator.initialize(Duration.ZERO, Duration.ZERO, Duration.ofSI(3600), createModelMock(),
147                 HistoryManagerDevs.noHistory(simulator));
148         Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
149         String networkId = "networkID";
150         trafficLightMap.put(trafficLightId, createTrafficLightMock(trafficLightId, networkId, simulator));
151         Network network = new Network(networkId, simulator);
152         network.addObject(trafficLightMap.get(trafficLightId));
153 
154         Duration cycleTime = Duration.ofSI(90);
155         Duration offset = Duration.ofSI(20);
156         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
157         ImmutableSet<String> ids = sg.getTrafficLightIds();
158         for (String tlId : ids)
159         {
160             assertTrue(trafficLightMap.containsKey(tlId), "returned id is in provided set");
161         }
162         for (String tlId : trafficLightMap.keySet())
163         {
164             assertTrue(ids.contains(tlId), "provided id is returned");
165         }
166         signalGroups.add(sg);
167         try
168         {
169             new FixedTimeController(null, simulator, network, cycleTime, offset, signalGroups);
170             fail("Null pointer for controller id should have thrown an exception");
171         }
172         catch (NullPointerException npe)
173         {
174             // Ignore
175         }
176         try
177         {
178             new FixedTimeController(ftcId, null, network, cycleTime, offset, signalGroups);
179             fail("Null pointer for simulator should have thrown an exception");
180         }
181         catch (NullPointerException npe)
182         {
183             // Ignore
184         }
185         try
186         {
187             new FixedTimeController(ftcId, simulator, null, cycleTime, offset, signalGroups);
188             fail("Null pointer for network should have thrown an exception");
189         }
190         catch (NullPointerException npe)
191         {
192             // Ignore
193         }
194         try
195         {
196             new FixedTimeController(ftcId, simulator, network, null, offset, signalGroups);
197             fail("Null pointer for cycle time should have thrown an exception");
198         }
199         catch (NullPointerException npe)
200         {
201             // Ignore
202         }
203         try
204         {
205             new FixedTimeController(ftcId, simulator, network, cycleTime, null, signalGroups);
206             fail("Null pointer for offset should have thrown an exception");
207         }
208         catch (NullPointerException npe)
209         {
210             // Ignore
211         }
212         try
213         {
214             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, null);
215             fail("Null pointer for signal groups should have thrown an exception");
216         }
217         catch (NullPointerException npe)
218         {
219             // Ignore
220         }
221         try
222         {
223             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, new LinkedHashSet<SignalGroup>());
224             fail("Empty signal groups should have thrown an exception");
225         }
226         catch (IllegalArgumentException iae)
227         {
228             // Ignore
229         }
230         try
231         {
232             new FixedTimeController(ftcId, simulator, network, Duration.ofSI(0), offset, signalGroups);
233             fail("Illegal cycle time should hav thrown an exception");
234         }
235         catch (IllegalArgumentException iae)
236         {
237             // Ignore
238         }
239         try
240         {
241             new FixedTimeController(ftcId, simulator, network, Duration.ofSI(-10), offset, signalGroups);
242             fail("Illegal cycle time should hav thrown an exception");
243         }
244         catch (IllegalArgumentException iae)
245         {
246             // Ignore
247         }
248         // Not testing check for identical signal groups; yet
249         // Now that we've tested all ways that the constructor should have told us to go to hell, create a controller
250         FixedTimeController ftc = new FixedTimeController(ftcId, simulator, network, cycleTime, offset, signalGroups);
251         assertEquals(ftcId, ftc.getId(), "FTC id");
252         assertTrue(ftc.toString().startsWith("FixedTimeController ["), "toString returns something descriptive");
253 
254         simulator.runUpTo(Duration.ONE);
255         while (simulator.isStartingOrRunning())
256         {
257             try
258             {
259                 Thread.sleep(100);
260             }
261             catch (InterruptedException exception)
262             {
263                 exception.printStackTrace();
264             }
265         }
266         for (TrafficLight tl : sg.getTrafficLights())
267         {
268             assertTrue(trafficLightMap.containsKey(tl.getId()), "acquired traffic light is in the proved set");
269         }
270         assertEquals(cycleTime.minus(preGreen).minus(green).minus(yellow).si, sg.getRed().si, 0.0001,
271                 "red time makes up remainder of cycle time");
272     }
273 
274     /**
275      * Test detection of non-disjoint sets of traffic lights.
276      * @throws NamingException on exception
277      * @throws SimRuntimeException on exception
278      * @throws NetworkException on exception
279      */
280     // TODO: @Test
281     public void testDisjoint() throws SimRuntimeException, NamingException, NetworkException
282     {
283         String signalGroupId = "sgId1";
284         Set<String> trafficLightIds1 = new LinkedHashSet<>();
285         String trafficLightId = "08.1";
286         trafficLightIds1.add(trafficLightId);
287         Duration signalGroupOffset = Duration.ofSI(5);
288         Duration preGreen = Duration.ofSI(2);
289         Duration green = Duration.ofSI(10);
290         Duration yellow = Duration.ofSI(3.5);
291         SignalGroup sg1 = new SignalGroup(signalGroupId, trafficLightIds1, signalGroupOffset, preGreen, green, yellow);
292         String signalGroupId2 = "sgId2";
293         Set<String> trafficLightIds2 = new LinkedHashSet<>();
294         trafficLightIds2.add(trafficLightId);
295         SignalGroup sg2 = new SignalGroup(signalGroupId2, trafficLightIds2, signalGroupOffset, preGreen, green, yellow);
296 
297         String ftcId = "FTCid";
298         OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
299         simulator.initialize(Duration.ZERO, Duration.ZERO, Duration.ofSI(3600), createModelMock(),
300                 HistoryManagerDevs.noHistory(simulator));
301         Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
302         String networkId = "networkID";
303         trafficLightMap.put(trafficLightId, createTrafficLightMock(trafficLightId, networkId, simulator));
304         Network network = new Network(networkId, simulator);
305         network.addObject(trafficLightMap.get(trafficLightId));
306 
307         Duration cycleTime = Duration.ofSI(90);
308         Duration offset = Duration.ofSI(20);
309         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
310         signalGroups.add(sg1);
311         signalGroups.add(sg2);
312         try
313         {
314             new FixedTimeController(ftcId, simulator, network, cycleTime, offset, signalGroups);
315             fail("Same traffic light in different signal groups should have thrown an IllegalArgumnentException");
316         }
317         catch (IllegalArgumentException iae)
318         {
319             // Ignore
320         }
321     }
322 
323     /**
324      * Test timing of fixed time controller.
325      * @throws SimRuntimeException if that happens uncaught; this test has failed
326      * @throws NamingException if that happens uncaught; this test has failed
327      * @throws NetworkException on exception
328      */
329     // TODO: @Test
330     public void testTimings() throws SimRuntimeException, NamingException, NetworkException
331     {
332         String signalGroupId = "sgId";
333         Set<String> trafficLightIds = new LinkedHashSet<>();
334         String trafficLightId = "08.1";
335         trafficLightIds.add(trafficLightId);
336         Set<SignalGroup> signalGroups = new LinkedHashSet<>();
337         for (int cycleTime : new int[] {60, 90})
338         {
339             Duration cycle = Duration.ofSI(cycleTime);
340             for (int ftcOffsetTime : new int[] {-100, -10, 0, 10, 100})
341             {
342                 Duration ftcOffset = Duration.ofSI(ftcOffsetTime);
343                 for (int sgOffsetTime : new int[] {-99, -9, 0, 9, 99})
344                 {
345                     Duration sgOffset = Duration.ofSI(sgOffsetTime);
346                     for (int preGreenTime : new int[] {0, 3})
347                     {
348                         Duration preGreen = Duration.ofSI(preGreenTime);
349                         for (int greenTime : new int[] {5, 15, 100})
350                         {
351                             Duration green = Duration.ofSI(greenTime);
352                             for (double yellowTime : new double[] {0, 3.5, 4.5})
353                             {
354                                 Duration yellow = Duration.ofSI(yellowTime);
355                                 double minimumCycleTime = preGreenTime + greenTime + yellowTime;
356                                 SignalGroup sg =
357                                         new SignalGroup(signalGroupId, trafficLightIds, sgOffset, preGreen, green, yellow);
358                                 signalGroups.clear();
359                                 signalGroups.add(sg);
360                                 String ftcId = "FTCid";
361                                 OtsSimulatorInterface simulator = new OtsSimulator("TestFixedTimeController");
362                                 simulator.initialize(Duration.ZERO, Duration.ZERO, Duration.ofSI(3600), createModelMock(),
363                                         HistoryManagerDevs.noHistory(simulator));
364                                 Map<String, TrafficLight> trafficLightMap = new LinkedHashMap<String, TrafficLight>();
365                                 String networkId = "networkID";
366                                 trafficLightMap.put(trafficLightId,
367                                         createTrafficLightMock(trafficLightId, networkId, simulator));
368                                 Network network = new Network(networkId, simulator);
369                                 network.addObject(trafficLightMap.get(trafficLightId));
370                                 // System.out.println(cycle);
371                                 FixedTimeController ftc =
372                                         new FixedTimeController(ftcId, simulator, network, cycle, ftcOffset, signalGroups);
373                                 // System.out.print(ftc);
374                                 if (cycleTime < minimumCycleTime)
375                                 {
376                                     PrintStream originalError = System.err;
377                                     boolean exceptionThrown = false;
378                                     try
379                                     {
380                                         while (simulator.getSimulatorTime().si <= 0)
381                                         {
382                                             System.setErr(new PrintStream(new ByteArrayOutputStream()));
383                                             try
384                                             {
385                                                 simulator.step();
386                                             }
387                                             finally
388                                             {
389                                                 System.setErr(originalError);
390                                             }
391                                         }
392                                     }
393                                     catch (SimRuntimeException exception)
394                                     {
395                                         exceptionThrown = true;
396                                         assertTrue(exception.getCause().getCause().getMessage().contains("Cycle time shorter "),
397                                                 "exception explains cycle time problem");
398                                     }
399                                     assertTrue(exceptionThrown,
400                                             "Too short cycle time should have thrown a SimRuntimeException");
401                                 }
402                                 else
403                                 {
404                                     // All transitions are at multiples of 0.5 seconds; check the state at 0.25 and 0.75 in each
405                                     // second
406                                     for (int second = 0; second <= 300; second++)
407                                     {
408                                         simulator.scheduleEventAbs(Duration.ofSI(second + 0.25),
409                                                 () -> checkState(simulator, ftc, true));
410                                         simulator.scheduleEventAbs(Duration.ofSI(second + 0.75),
411                                                 () -> checkState(simulator, ftc, true));
412                                     }
413                                     Duration stopTime = Duration.ofSI(300.0);
414                                     simulator.runUpTo(stopTime);
415                                     while (simulator.isStartingOrRunning())
416                                     {
417                                         try
418                                         {
419                                             Thread.sleep(1);
420                                         }
421                                         catch (InterruptedException exception)
422                                         {
423                                             exception.printStackTrace();
424                                         }
425                                     }
426                                     if (simulator.getSimulatorTime().lt(stopTime))
427                                     {
428                                         // something went wrong; call checkState with stopSimulatorOnError set to false
429                                         checkState(simulator, ftc, Boolean.FALSE);
430                                         fail("checkState should have thrown an assert error");
431                                     }
432                                 }
433                             }
434                         }
435                     }
436                 }
437             }
438         }
439     }
440 
441     /**
442      * Check that the current state of a fixed time traffic light controller matches the design.
443      * @param simulator the simulator
444      * @param ftc the fixed time traffic light controller
445      * @param stopSimulatorOnError if true; stop the simulator on error; if false; execute the failing assert on error
446      */
447     public void checkState(final OtsSimulatorInterface simulator, final FixedTimeController ftc,
448             final boolean stopSimulatorOnError)
449     {
450         double cycleTime = ftc.getCycleTime().si;
451         double time = simulator.getSimulatorTime().si;
452         double mainOffset = ftc.getOffset().si;
453         for (SignalGroup sg : ftc.getSignalGroups())
454         {
455             double phaseOffset = sg.getOffset().si + mainOffset;
456             double phase = time + phaseOffset;
457             while (phase < 0)
458             {
459                 phase += cycleTime;
460             }
461             phase %= cycleTime;
462             TrafficLightColor expectedColor = null;
463             if (phase < sg.getPreGreen().si)
464             {
465                 expectedColor = TrafficLightColor.PREGREEN;
466             }
467             else if (phase < sg.getPreGreen().plus(sg.getGreen()).si)
468             {
469                 expectedColor = TrafficLightColor.GREEN;
470             }
471             else if (phase < sg.getPreGreen().plus(sg.getGreen()).plus(sg.getYellow()).si)
472             {
473                 expectedColor = TrafficLightColor.YELLOW;
474             }
475             else
476             {
477                 expectedColor = TrafficLightColor.RED;
478             }
479             // Verify the color of all traffic lights
480             for (TrafficLight tl : sg.getTrafficLights())
481             {
482                 if (!expectedColor.equals(tl.getTrafficLightColor()))
483                 {
484                     if (stopSimulatorOnError)
485                     {
486                         try
487                         {
488                             simulator.stop();
489                         }
490                         catch (SimRuntimeException exception)
491                         {
492                             exception.printStackTrace();
493                         }
494                     }
495                     else
496                     {
497                         assertEquals(expectedColor + " which is in phase " + phase + " of cycle time " + cycleTime,
498                                 tl.getTrafficLightColor(), "Traffic light color mismatch at simulator time "
499                                         + simulator.getSimulatorTime() + " of signal group " + sg);
500                     }
501                 }
502             }
503         }
504     }
505 
506     /**
507      * Create a mocked OtsModelInterface.
508      * @return OtsModelInterface
509      */
510     public OtsModelInterface createModelMock()
511     {
512         return Mockito.mock(OtsModelInterface.class);
513     }
514 
515     /** Remember current state of all mocked traffic lights. */
516     private Map<String, TrafficLightColor> currentTrafficLightColors = new LinkedHashMap<>();
517 
518     /**
519      * Mock a traffic light.
520      * @param id value that will be returned by the getId method
521      * @param networkId name of network (prepended to id for result of getFullId method)
522      * @param simulator TODO
523      * @return TrafficLight
524      */
525     public TrafficLight createTrafficLightMock(final String id, final String networkId, final OtsSimulatorInterface simulator)
526     {
527         TrafficLight result = Mockito.mock(TrafficLight.class);
528         Mockito.when(result.getId()).thenReturn(id);
529         Mockito.when(result.getFullId()).thenReturn(networkId + "." + id);
530         Mockito.when(result.getTrafficLightColor()).thenAnswer(new Answer<TrafficLightColor>()
531         {
532             @Override
533             public TrafficLightColor answer(final InvocationOnMock invocation) throws Throwable
534             {
535                 return TestFixedTimeController.this.currentTrafficLightColors.get(result.getFullId());
536             }
537         });
538         Mockito.doAnswer((Answer<Void>) invocation ->
539         {
540             TrafficLightColor tlc = invocation.getArgument(0);
541             // System.out.println(simulator.getSimulatorTime() + " changing color of " + result.getFullId() + " from "
542             // + this.currentTrafficLightColors.get(result.getFullId()) + " to " + tlc);
543             this.currentTrafficLightColors.put(result.getFullId(), tlc);
544             return null;
545         }).when(result).setTrafficLightColor(ArgumentMatchers.any(TrafficLightColor.class));
546         this.currentTrafficLightColors.put(result.getFullId(), TrafficLightColor.BLACK);
547         return result;
548     }
549 
550 }