View Javadoc
1   package org.opentrafficsim.road.gtu.perception;
2   
3   import static org.junit.Assert.fail;
4   
5   import java.lang.reflect.Field;
6   import java.lang.reflect.Method;
7   import java.lang.reflect.Modifier;
8   import java.util.ArrayList;
9   import java.util.Collection;
10  import java.util.LinkedHashSet;
11  import java.util.List;
12  import java.util.Set;
13  
14  import org.junit.Test;
15  import org.opentrafficsim.base.TimeStampedObject;
16  import org.opentrafficsim.core.gtu.perception.AbstractPerceptionCategory;
17  import org.opentrafficsim.road.ClassList;
18  
19  /**
20   * <p>
21   * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
22   * BSD-style license. See <a href="http://opentrafficsim.org/docs/current/license.html">OpenTrafficSim License</a>.
23   * <p>
24   * @version $Revision$, $LastChangedDate$, by $Author$, initial version Jul 22, 2016 <br>
25   * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
26   * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
27   * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
28   */
29  
30  public class VerifyPerceptionCategoryMethods
31  {
32  
33      /**
34       * Check that all sub-classes of AbstractPerceptionCategory have for data named {@code TestField}:
35       * <ul>
36       * <li>{@code testField*} property of any type. Data may be organized e.g. per lane, so type is not forced to be
37       * {@code TimeStampedObject}. Data may also be stored as e.g. {@code testFieldLeft} and {@code testFieldRight}, hence the
38       * {@code *}. <br>
39       * If the field is not found, wrapped {@code AbstractPerceptionCategory} fields are checked. If either has the property the
40       * test succeeds. Methods should still be in place as forwards to the wrapped category.</li>
41       * <li>{@code updateTestField} method</li>
42       * <li>For boolean:
43       * <ul>
44       * <li>{@code isTestField} method returning {@code boolean}.</li>
45       * <li>{@code isTestFieldTimeStamped} method returning {@code TimeStampedObject}.</li>
46       * </ul>
47       * </li>
48       * <li>For non-boolean:
49       * <ul>
50       * <li>{@code getTestField} method <b>not</b> returning {@code void}.</li>
51       * <li>{@code getTimeStampedTestField} method returning {@code TimeStampedObject}.</li>
52       * </ul>
53       * </li>
54       * </ul>
55       * These tests are performed whenever a public method with these naming patterns is encountered:
56       * <ul>
57       * <li>{@code is*} (boolean).</li>
58       * <li>{@code is*TimeStamped} (boolean).</li>
59       * <li>{@code get*} (non-boolean).</li>
60       * <li>{@code getTimeStamped*} (non-boolean).</li>
61       * </ul>
62       * The {@code *} is subtracted as field name, with first character made upper or lower case as by convention.
63       */
64      @Test
65      public final void perceptionCategoryTest()
66      {
67          // TODO: to what extent do we want to prescribe this now that we have more flexible perception categories
68          Collection<Class<?>> classList = ClassList.classList("org.opentrafficsim", true);
69          for (Class<?> c : classList)
70          {
71              if (AbstractPerceptionCategory.class.isAssignableFrom(c) && !Modifier.isAbstract(c.getModifiers()))
72              {
73                  Set<String> fieldsDone = new LinkedHashSet<>();
74                  List<String> fieldNames = new ArrayList<>();
75                  List<String> methodNames = new ArrayList<>();
76                  List<Class<?>> methodReturnTypes = new ArrayList<>();
77                  for (Field field : c.getDeclaredFields())
78                  {
79                      fieldNames.add(field.getName());
80                  }
81                  for (Method method : c.getMethods())
82                  {
83                      methodNames.add(method.getName());
84                      methodReturnTypes.add(method.getReturnType());
85                  }
86                  for (Method method : c.getDeclaredMethods())
87                  {
88                      if (Modifier.isPrivate(method.getModifiers()))
89                      {
90                          continue;
91                      }
92                      String name = method.getName();
93                      String field = null;
94                      boolean isBoolean = false;
95                      if (name.startsWith("is") && name.endsWith("TimeStamped"))
96                      {
97                          field = name.substring(2, name.length() - 11);
98                          isBoolean = true;
99                      }
100                     else if (name.startsWith("is"))
101                     {
102                         field = name.substring(2);
103                         isBoolean = true;
104                     }
105                     else if (name.startsWith("getTimeStamped"))
106                     {
107                         field = name.substring(14);
108                     }
109                     else if (name.startsWith("get"))
110                     {
111                         field = name.substring(3);
112                     }
113 
114                     if (field != null)
115                     {
116                         String fieldDown = field.substring(0, 1).toLowerCase() + field.substring(1);
117                         if (!fieldsDone.contains(fieldDown))
118                         {
119                             String fieldUp = field.substring(0, 1).toUpperCase() + field.substring(1);
120                             if (isBoolean)
121                             {
122                                 testGetter(c, fieldNames, methodNames, methodReturnTypes, fieldDown, "is" + fieldUp,
123                                         "is" + fieldUp + "TimeStamped", "update" + fieldUp);
124                             }
125                             else
126                             {
127                                 testGetter(c, fieldNames, methodNames, methodReturnTypes, fieldDown, "get" + fieldUp,
128                                         "getTimeStamped" + fieldUp, "update" + fieldUp);
129                             }
130                             fieldsDone.add(fieldDown);
131                         }
132                     }
133 
134                 }
135             }
136         }
137     }
138 
139     /**
140      * @param c class that is checked, subclass of AbstractPerceptionCategory
141      * @param fieldNames field names of c
142      * @param methodNames method names of c
143      * @param methodReturnTypes return types of methods of c
144      * @param field field that should be present
145      * @param getter regular getter/is method that should be present
146      * @param timeStampedGetter time stamped getter/is method that should be present
147      * @param updater update method that should be present
148      */
149     @SuppressWarnings("checkstyle:parameternumber")
150     private void testGetter(final Class<?> c, final List<String> fieldNames, final List<String> methodNames,
151             final List<Class<?>> methodReturnTypes, final String field, final String getter, final String timeStampedGetter,
152             final String updater)
153     {
154         boolean fieldFound = false;
155         int i = 0;
156         while (!fieldFound && i < fieldNames.size())
157         {
158             fieldFound = fieldNames.get(i).startsWith(field);
159             i++;
160         }
161         if (!fieldFound)
162         {
163             // perhaps the perception category wraps another category
164             Field[] fields = c.getDeclaredFields();
165             i = 0;
166             while (!fieldFound && i < fields.length)
167             {
168                 if (AbstractPerceptionCategory.class.isAssignableFrom(fields[i].getType()))
169                 {
170                     // check if this wrapped category has the right field
171                     Field[] wrappedFields = fields[i].getType().getDeclaredFields();
172                     int j = 0;
173                     while (!fieldFound && j < wrappedFields.length)
174                     {
175                         fieldFound = wrappedFields[j].getName().startsWith(field);
176                         j++;
177                     }
178                 }
179                 i++;
180             }
181         }
182         if (!fieldFound)
183         {
184             // System.out.println("Class " + c.getSimpleName() + " does not have a field '" + field + "'.");
185             // TODO: fail("Class " + c + " does not have a field '" + field + "*', nor wraps a perception category that does.");
186         }
187         if (methodNames.contains(getter))
188         {
189             if (getter.startsWith("is") && !methodReturnTypes.get(methodNames.indexOf(getter)).equals(boolean.class))
190             {
191                 fail("Class " + c + "'s method '" + getter + "' does not return a boolean.");
192             }
193             else if (methodReturnTypes.get(methodNames.indexOf(getter)).equals(void.class))
194             {
195                 fail("Class " + c + "'s method '" + getter + "' does not return anything.");
196             }
197             // System.out.println("Class " + c.getSimpleName() + "'s method " + getter + " has correct return type.");
198         }
199         else
200         {
201             fail("Class " + c + " does not contain a method '" + getter + "'.");
202         }
203         if (methodNames.contains(timeStampedGetter))
204         {
205             if (!methodReturnTypes.get(methodNames.indexOf(timeStampedGetter)).equals(TimeStampedObject.class))
206             {
207                 fail("Class " + c + "'s method '" + timeStampedGetter + "' does not return a TimeStampedObject.");
208             }
209             // System.out
210             // .println("Class " + c.getSimpleName() + "'s method " + timeStampedGetter + " has correct return type.");
211         }
212         else
213         {
214             // Accept that no time-stamped method is present
215             // System.err.println("Class " + c + " does not contain a method '" + timeStampedGetter + "'.");
216             // TODO: fail...
217         }
218         if (!methodNames.contains(updater))
219         {
220             // System.out.println("Class " + c.getSimpleName() + " does not contain a method '" + updater + "'.");
221             // System.err.print("Class " + c + " does not contain a method '" + updater + "'.");
222             // TODO: fail...
223         }
224     }
225 
226     /**
227      * @param args arguments
228      */
229     public static void main(final String[] args)
230     {
231         VerifyPerceptionCategoryMethods t = new VerifyPerceptionCategoryMethods();
232         t.perceptionCategoryTest();
233     }
234 
235 }