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-2020 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 VerifyPerceptionCategoryMethodsPerceptionCategoryMethods.html#VerifyPerceptionCategoryMethods">VerifyPerceptionCategoryMethods t = new VerifyPerceptionCategoryMethods();
232 t.perceptionCategoryTest();
233 }
234
235 }