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