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 }