1/* 2 * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24import java.lang.annotation.RetentionPolicy; 25import java.util.*; 26import java.util.stream.Collectors; 27 28public class TestCase { 29 30 /** 31 * The top-level classes of the test case. 32 */ 33 public final Map<String, TestClassInfo> classes = new LinkedHashMap<>(); 34 35 /** 36 * Constructs a test class info with {@code classType} as top-level class, 37 * with {@code outerClassName} as name and {@code mods} as modifiers. 38 * 39 * @param classType a class type 40 * @param outerClassName a name 41 * @param mods an array of modifiers 42 */ 43 public TestClassInfo addClassInfo(ClassType classType, String outerClassName, String...mods) { 44 return addClassInfo(null, classType, outerClassName, mods); 45 } 46 47 /** 48 * Constructs a test class info with {@code classType} as top-level class, 49 * with {@code outerClassName} as name, {@code parent} class name 50 * as parent class and {@code mods} as modifiers. 51 * 52 * @param classType a class type 53 * @param outerClassName a name 54 * @param mods an array of modifiers 55 */ 56 public TestClassInfo addClassInfo(String parent, ClassType classType, String outerClassName, String...mods) { 57 TestClassInfo clazz = new TestClassInfo(classType, outerClassName, parent, mods); 58 if (classes.put(outerClassName, clazz) != null) { 59 throw new IllegalArgumentException("Duplicate class name: " + outerClassName); 60 } 61 return clazz; 62 } 63 64 public String generateSource() { 65 return classes.values().stream() 66 .map(TestMemberInfo::generateSource) 67 .collect(Collectors.joining("\n")); 68 } 69 70 /** 71 * Returns {@code TestClassInfo} by class signature. 72 * Example, {@code getTestClassInfo("Test$1Local")} 73 * returns local inner class of class {@code Test}. 74 * 75 * @param classSignature a class signature 76 * @return {@code TestClassInfo} by class signature 77 */ 78 public TestClassInfo getTestClassInfo(String classSignature) { 79 String[] cs = classSignature.split("\\$"); 80 if (cs.length > 0 && classes.containsKey(cs[0])) { 81 // check signature corresponds to top level class 82 if (cs.length == 1) { 83 return classes.get(cs[0]); 84 } 85 } else { 86 throw new IllegalArgumentException("Cannot find class : " + classSignature); 87 } 88 TestClassInfo current = classes.get(cs[0]); 89 // find class info in the inner classes 90 for (int i = 1; i < cs.length; ++i) { 91 Map<String, TestClassInfo> innerClasses = current.innerClasses; 92 Map<String, TestMethodInfo> methods = current.methods; 93 current = innerClasses.get(cs[i]); 94 // if current is null then class info does not exist or the class is local 95 if (current == null) { 96 if (!cs[i].isEmpty()) { 97 // the class is local, remove leading digit 98 String className = cs[i].substring(1); 99 Optional<TestClassInfo> opt = methods.values().stream() 100 .flatMap(c -> c.localClasses.values().stream()) 101 .filter(c -> c.name.equals(className)).findAny(); 102 if (opt.isPresent()) { 103 current = opt.get(); 104 // continue analysis of local class 105 continue; 106 } 107 } 108 throw new IllegalArgumentException("Cannot find class : " + classSignature); 109 } 110 } 111 return current; 112 } 113 114 /** 115 * Class represents a program member. 116 */ 117 public static abstract class TestMemberInfo { 118 // next two fields are used for formatting 119 protected final int indention; 120 protected final ClassType containerType; 121 public final List<String> mods; 122 public final String name; 123 public final Map<String, TestAnnotationInfo> annotations; 124 125 TestMemberInfo(int indention, ClassType containerType, String name, String... mods) { 126 this.indention = indention; 127 this.containerType = containerType; 128 this.mods = Arrays.asList(mods); 129 this.name = name; 130 this.annotations = new HashMap<>(); 131 } 132 133 public abstract String generateSource(); 134 135 public boolean isAnnotated(RetentionPolicy policy) { 136 return annotations.values().stream() 137 .filter(a -> a.policy == policy) 138 .findAny().isPresent(); 139 } 140 141 public Set<String> getRuntimeVisibleAnnotations() { 142 return getRuntimeAnnotations(RetentionPolicy.RUNTIME); 143 } 144 145 public Set<String> getRuntimeInvisibleAnnotations() { 146 return getRuntimeAnnotations(RetentionPolicy.CLASS); 147 } 148 149 private Set<String> getRuntimeAnnotations(RetentionPolicy policy) { 150 return annotations.values().stream() 151 .filter(e -> e.policy == policy) 152 .map(a -> a.annotationName) 153 .distinct() 154 .collect(Collectors.toSet()); 155 } 156 157 /** 158 * Generates source for annotations. 159 * 160 * @param prefix a leading text 161 * @param suffix a trailing text 162 * @param joining a text between annotations 163 * @return source for annotations 164 */ 165 protected String generateSourceForAnnotations(String prefix, String suffix, String joining) { 166 StringBuilder sb = new StringBuilder(); 167 for (TestAnnotationInfo annotation : annotations.values()) { 168 sb.append(prefix); 169 if (annotation.isContainer) { 170 // the annotation is repeatable 171 // container consists of an array of annotations 172 TestAnnotationInfo.TestArrayElementValue containerElementValue = 173 (TestAnnotationInfo.TestArrayElementValue) annotation.elementValues.get(0).elementValue; 174 // concatenate sources of repeatable annotations 175 sb.append(containerElementValue.values.stream() 176 .map(TestAnnotationInfo.TestElementValue::toString) 177 .collect(Collectors.joining(joining))); 178 } else { 179 sb.append(annotation); 180 } 181 sb.append(suffix); 182 } 183 String src = sb.toString(); 184 return src.trim().isEmpty() ? "" : src; 185 186 } 187 188 /** 189 * Generates source for annotations. 190 * 191 * @return source for annotations 192 */ 193 public String generateSourceForAnnotations() { 194 return generateSourceForAnnotations(indention(), "\n", "\n" + indention()); 195 } 196 197 /** 198 * Adds annotation info to the member. 199 * 200 * @param anno an annotation info 201 */ 202 public void addAnnotation(TestAnnotationInfo anno) { 203 String containerName = anno.annotationName + "Container"; 204 TestAnnotationInfo annotation = annotations.get(anno.annotationName); 205 TestAnnotationInfo containerAnnotation = annotations.get(containerName); 206 207 if (annotation == null) { 208 // if annotation is null then either it is first adding of the annotation to the member 209 // or there is the container of the annotation. 210 if (containerAnnotation == null) { 211 // first adding to the member 212 annotations.put(anno.annotationName, anno); 213 } else { 214 // add annotation to container 215 TestAnnotationInfo.TestArrayElementValue containerElementValue = 216 ((TestAnnotationInfo.TestArrayElementValue) containerAnnotation.elementValues.get(0).elementValue); 217 containerElementValue.values.add(new TestAnnotationInfo.TestAnnotationElementValue(anno.annotationName, anno)); 218 } 219 } else { 220 // remove previously added annotation and add new container of repeatable annotation 221 // which contains previously added and new annotation 222 annotations.remove(anno.annotationName); 223 containerAnnotation = new TestAnnotationInfo( 224 containerName, 225 anno.policy, 226 true, 227 new TestAnnotationInfo.Pair("value", 228 new TestAnnotationInfo.TestArrayElementValue( 229 new TestAnnotationInfo.TestAnnotationElementValue(anno.annotationName, annotation), 230 new TestAnnotationInfo.TestAnnotationElementValue(anno.annotationName, anno)))); 231 annotations.put(containerName, containerAnnotation); 232 } 233 } 234 235 public String indention() { 236 char[] a = new char[4 * indention]; 237 Arrays.fill(a, ' '); 238 return new String(a); 239 } 240 241 public String getName() { 242 return name; 243 } 244 } 245 246 /** 247 * The class represents a class. 248 */ 249 public static class TestClassInfo extends TestMemberInfo { 250 public final ClassType classType; 251 public final String parent; 252 public final Map<String, TestClassInfo> innerClasses; 253 public final Map<String, TestMethodInfo> methods; 254 public final Map<String, TestFieldInfo> fields; 255 256 TestClassInfo(int indention, ClassType classType, String className, String... mods) { 257 this(indention, classType, className, null, mods); 258 } 259 260 TestClassInfo(ClassType classType, String className, String parent, String... mods) { 261 this(0, classType, className, parent, mods); 262 } 263 264 TestClassInfo(int indention, ClassType classType, String className, String parent, String... mods) { 265 super(indention, null, className, mods); 266 this.classType = classType; 267 this.parent = parent; 268 innerClasses = new LinkedHashMap<>(); 269 methods = new LinkedHashMap<>(); 270 fields = new LinkedHashMap<>(); 271 } 272 273 /** 274 * Generates source which represents the class. 275 * 276 * @return source which represents the class 277 */ 278 @Override 279 public String generateSource() { 280 String sourceForAnnotations = generateSourceForAnnotations(); 281 String classModifiers = mods.stream().collect(Collectors.joining(" ")); 282 return sourceForAnnotations 283 + String.format("%s%s %s %s %s {%n", 284 indention(), 285 classModifiers, 286 classType.getDescription(), 287 name, 288 parent == null ? "" : "extends " + parent) 289 + classType.collectFields(fields.values()) 290 + classType.collectMethods(methods.values()) 291 + classType.collectInnerClasses(innerClasses.values()) 292 + indention() + "}"; 293 } 294 295 /** 296 * Adds a new inner class to the class. 297 * 298 * @param classType a class type 299 * @param className a class name 300 * @param mods modifiers 301 * @return a new added inner class to the class 302 */ 303 public TestClassInfo addInnerClassInfo(ClassType classType, String className, String... mods) { 304 TestClassInfo testClass = new TestClassInfo(indention + 1, classType, className, mods); 305 if (innerClasses.put(className, testClass) != null) { 306 throw new IllegalArgumentException("Duplicated class : " + className); 307 } 308 return testClass; 309 } 310 311 /** 312 * Adds a new method to the class. 313 * 314 * @param methodName a method name 315 * @param mods modifiers 316 * @return a new inner class to the class 317 */ 318 public TestMethodInfo addMethodInfo(String methodName, String... mods) { 319 return addMethodInfo(methodName, false, mods); 320 } 321 322 /** 323 * Adds a new method to the class. 324 * 325 * @param methodName a method name 326 * @param isSynthetic if {@code true} the method is synthetic 327 * @param mods modifiers 328 * @return a new method added to the class 329 */ 330 public TestMethodInfo addMethodInfo(String methodName, boolean isSynthetic, String... mods) { 331 boolean isConstructor = methodName.contains("<init>"); 332 if (isConstructor) { 333 methodName = methodName.replace("<init>", name); 334 } 335 TestMethodInfo testMethod = new TestMethodInfo(indention + 1, classType, methodName, isConstructor, isSynthetic, mods); 336 if (methods.put(methodName, testMethod) != null) { 337 throw new IllegalArgumentException("Duplicated method : " + methodName); 338 } 339 return testMethod; 340 } 341 342 /** 343 * Adds a new field to the class. 344 * 345 * @param fieldName a method name 346 * @param mods modifiers 347 * @return a new field added to the class 348 */ 349 public TestFieldInfo addFieldInfo(String fieldName, String... mods) { 350 TestFieldInfo field = new TestFieldInfo(indention + 1, classType, fieldName, mods); 351 if (fields.put(fieldName, field) != null) { 352 throw new IllegalArgumentException("Duplicated field : " + fieldName); 353 } 354 return field; 355 } 356 357 public TestMethodInfo getTestMethodInfo(String methodName) { 358 return methods.get(methodName); 359 } 360 361 public TestFieldInfo getTestFieldInfo(String fieldName) { 362 return fields.get(fieldName); 363 } 364 } 365 366 public static class TestMethodInfo extends TestMemberInfo { 367 public final boolean isConstructor; 368 public final boolean isSynthetic; 369 public final Map<String, TestClassInfo> localClasses; 370 public final List<TestParameterInfo> parameters; 371 372 TestMethodInfo(int indention, ClassType containerType, String methodName, 373 boolean isConstructor, boolean isSynthetic, String... mods) { 374 super(indention, containerType, methodName, mods); 375 this.isSynthetic = isSynthetic; 376 this.localClasses = new LinkedHashMap<>(); 377 this.parameters = new ArrayList<>(); 378 this.isConstructor = isConstructor; 379 } 380 381 public boolean isParameterAnnotated(RetentionPolicy policy) { 382 return parameters.stream() 383 .filter(p -> p.isAnnotated(policy)) 384 .findFirst().isPresent(); 385 } 386 387 public TestParameterInfo addParameter(String type, String name) { 388 TestParameterInfo testParameter = new TestParameterInfo(type, name); 389 parameters.add(testParameter); 390 return testParameter; 391 } 392 393 /** 394 * Adds a local class to the method. 395 * 396 * @param className a class name 397 * @param mods modifiers 398 * @return a local class added to the method 399 */ 400 public TestClassInfo addLocalClassInfo(String className, String... mods) { 401 TestClassInfo testClass = new TestClassInfo(indention + 1, ClassType.CLASS, className, mods); 402 if (localClasses.put(className, testClass) != null) { 403 throw new IllegalArgumentException("Duplicated class : " + className); 404 } 405 return testClass; 406 } 407 408 @Override 409 public String generateSource() { 410 if (isSynthetic) { 411 return ""; 412 } 413 return generateSourceForAnnotations() + 414 containerType.methodToString(this); 415 } 416 417 @Override 418 public String getName() { 419 return name.replaceAll("\\(.*\\)", ""); 420 } 421 } 422 423 /** 424 * The class represents a method parameter. 425 */ 426 public static class TestParameterInfo extends TestMemberInfo { 427 public final String type; 428 429 TestParameterInfo(String type, String name) { 430 super(0, null, name); 431 this.type = type; 432 } 433 434 @Override 435 public String generateSource() { 436 return generateSourceForAnnotations() + type + " " + name; 437 } 438 439 public String generateSourceForAnnotations() { 440 return generateSourceForAnnotations("", " ", " "); 441 } 442 } 443 444 /** 445 * The class represents a field. 446 */ 447 public static class TestFieldInfo extends TestMemberInfo { 448 449 TestFieldInfo(int indention, ClassType containerType, String fieldName, String... mods) { 450 super(indention, containerType, fieldName, mods); 451 } 452 453 @Override 454 public String generateSource() { 455 return generateSourceForAnnotations() + 456 containerType.fieldToString(this); 457 } 458 } 459} 460