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