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-2023 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://dittlab.tudelft.nl">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 }