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 
24 import java.lang.annotation.RetentionPolicy;
25 import java.util.*;
26 import java.util.stream.Collectors;
27 
28 public 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      */
addClassInfo(ClassType classType, String outerClassName, String...mods)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      */
addClassInfo(String parent, ClassType classType, String outerClassName, String...mods)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 
generateSource()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(&quot;Test$1Local&quot;)}
73      * returns local inner class of class {@code Test}.
74      *
75      * @param classSignature a class signature
76      * @return {@code TestClassInfo} by class signature
77      */
getTestClassInfo(String classSignature)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 
TestMemberInfo(int indention, ClassType containerType, String name, String... mods)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 
generateSource()133         public abstract String generateSource();
134 
isAnnotated(RetentionPolicy policy)135         public boolean isAnnotated(RetentionPolicy policy) {
136             return annotations.values().stream()
137                     .filter(a -> a.policy == policy)
138                     .findAny().isPresent();
139         }
140 
getRuntimeVisibleAnnotations()141         public Set<String> getRuntimeVisibleAnnotations() {
142             return getRuntimeAnnotations(RetentionPolicy.RUNTIME);
143         }
144 
getRuntimeInvisibleAnnotations()145         public Set<String> getRuntimeInvisibleAnnotations() {
146             return getRuntimeAnnotations(RetentionPolicy.CLASS);
147         }
148 
getRuntimeAnnotations(RetentionPolicy policy)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          */
generateSourceForAnnotations(String prefix, String suffix, String joining)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          */
generateSourceForAnnotations()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          */
addAnnotation(TestAnnotationInfo anno)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 
indention()235         public String indention() {
236             char[] a = new char[4 * indention];
237             Arrays.fill(a, ' ');
238             return new String(a);
239         }
240 
getName()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 
TestClassInfo(int indention, ClassType classType, String className, String... mods)256         TestClassInfo(int indention, ClassType classType, String className, String... mods) {
257             this(indention, classType, className, null, mods);
258         }
259 
TestClassInfo(ClassType classType, String className, String parent, String... mods)260         TestClassInfo(ClassType classType, String className, String parent, String... mods) {
261             this(0, classType, className, parent, mods);
262         }
263 
TestClassInfo(int indention, ClassType classType, String className, String parent, String... mods)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
generateSource()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          */
addInnerClassInfo(ClassType classType, String className, String... mods)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          */
addMethodInfo(String methodName, String... mods)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          */
addMethodInfo(String methodName, boolean isSynthetic, String... mods)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          */
addFieldInfo(String fieldName, String... mods)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 
getTestMethodInfo(String methodName)357         public TestMethodInfo getTestMethodInfo(String methodName) {
358             return methods.get(methodName);
359         }
360 
getTestFieldInfo(String fieldName)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 
TestMethodInfo(int indention, ClassType containerType, String methodName, boolean isConstructor, boolean isSynthetic, String... mods)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 
isParameterAnnotated(RetentionPolicy policy)381         public boolean isParameterAnnotated(RetentionPolicy policy) {
382             return parameters.stream()
383                     .filter(p -> p.isAnnotated(policy))
384                     .findFirst().isPresent();
385         }
386 
addParameter(String type, String name)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          */
addLocalClassInfo(String className, String... mods)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
generateSource()409         public String generateSource() {
410             if (isSynthetic) {
411                 return "";
412             }
413             return generateSourceForAnnotations() +
414                     containerType.methodToString(this);
415         }
416 
417         @Override
getName()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 
TestParameterInfo(String type, String name)429         TestParameterInfo(String type, String name) {
430             super(0, null, name);
431             this.type = type;
432         }
433 
434         @Override
generateSource()435         public String generateSource() {
436             return generateSourceForAnnotations() + type + " " + name;
437         }
438 
generateSourceForAnnotations()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 
TestFieldInfo(int indention, ClassType containerType, String fieldName, String... mods)449         TestFieldInfo(int indention, ClassType containerType, String fieldName, String... mods) {
450             super(indention, containerType, fieldName, mods);
451         }
452 
453         @Override
generateSource()454         public String generateSource() {
455             return generateSourceForAnnotations() +
456                     containerType.fieldToString(this);
457         }
458     }
459 }
460