1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import com.android.tradefed.util.AbiUtils;
18
19import org.junit.runner.RunWith;
20import org.w3c.dom.Document;
21import org.w3c.dom.Element;
22import org.w3c.dom.Node;
23import org.w3c.dom.NodeList;
24
25import vogar.Expectation;
26import vogar.ExpectationStore;
27
28import java.io.BufferedReader;
29import java.io.File;
30import java.io.FileInputStream;
31import java.io.FileReader;
32import java.io.IOException;
33import java.lang.annotation.Annotation;
34import java.lang.reflect.Method;
35import java.lang.reflect.Modifier;
36import java.util.ArrayList;
37import java.util.Enumeration;
38import java.util.HashSet;
39import java.util.Iterator;
40import java.util.LinkedHashMap;
41import java.util.Map;
42import java.util.Set;
43import java.util.jar.JarEntry;
44import java.util.jar.JarFile;
45
46import javax.xml.parsers.DocumentBuilderFactory;
47import javax.xml.parsers.ParserConfigurationException;
48
49import junit.framework.TestCase;
50
51public class CollectAllTests extends DescriptionGenerator {
52
53    private static final String ATTRIBUTE_RUNNER = "runner";
54    private static final String ATTRIBUTE_PACKAGE = "appPackageName";
55    private static final String ATTRIBUTE_NS = "appNameSpace";
56    private static final String ATTRIBUTE_TARGET = "targetNameSpace";
57    private static final String ATTRIBUTE_TARGET_BINARY = "targetBinaryName";
58    private static final String ATTRIBUTE_HOST_SIDE_ONLY = "hostSideOnly";
59    private static final String ATTRIBUTE_VM_HOST_TEST = "vmHostTest";
60    private static final String ATTRIBUTE_JAR_PATH = "jarPath";
61    private static final String ATTRIBUTE_JAVA_PACKAGE_FILTER = "javaPackageFilter";
62
63    private static final String JAR_PATH = "LOCAL_JAR_PATH :=";
64    private static final String TEST_TYPE = "LOCAL_TEST_TYPE :";
65
66    public static void main(String[] args) {
67        if (args.length < 5 || args.length > 7) {
68            System.err.println("usage: CollectAllTests <output-file> <manifest-file> <jar-file> "
69                               + "<java-package> <architecture> [expectation-dir [makefile-file]]");
70            if (args.length != 0) {
71                System.err.println("received:");
72                for (String arg : args) {
73                    System.err.println("  " + arg);
74                }
75            }
76            System.exit(1);
77        }
78
79        final String outputPathPrefix = args[0];
80        File manifestFile = new File(args[1]);
81        String jarFileName = args[2];
82        final String javaPackageFilterArg = args[3] != null ? args[3].replaceAll("\\s+", "") : "";
83        final String[] javaPackagePrefixes;
84        // Validate the javaPackageFilter value if non-empty.
85        if (!javaPackageFilterArg.isEmpty()) {
86            javaPackagePrefixes = javaPackageFilterArg.split(":");
87            for (int i = 0; i < javaPackagePrefixes.length; ++i) {
88                final String javaPackageFilter = javaPackagePrefixes[i];
89                if (!isValidJavaPackage(javaPackageFilter)) {
90                    System.err.println("Invalid " + ATTRIBUTE_JAVA_PACKAGE_FILTER + ": " +
91                           javaPackageFilter);
92                    System.exit(1);
93                    return;
94                } else {
95                    javaPackagePrefixes[i] = javaPackageFilter.trim() + ".";
96                }
97            }
98        } else {
99            javaPackagePrefixes = new String[0];
100        }
101
102        String architecture = args[4];
103        if (architecture == null || architecture.equals("")) {
104            System.err.println("Invalid architecture");
105            System.exit(1);
106            return;
107        }
108        String libcoreExpectationDir = (args.length > 5) ? args[5] : null;
109        String androidMakeFile = (args.length > 6) ? args[6] : null;
110
111        final TestType testType = TestType.getTestType(androidMakeFile);
112
113        Document manifest;
114        try {
115            manifest = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
116                  new FileInputStream(manifestFile));
117        } catch (Exception e) {
118            System.err.println("cannot open manifest " + manifestFile);
119            e.printStackTrace();
120            System.exit(1);
121            return;
122        }
123
124        Element documentElement = manifest.getDocumentElement();
125        documentElement.getAttribute("package");
126        final String runner = getElementAttribute(documentElement,
127                                                  "instrumentation",
128                                                  "android:name");
129        final String packageName = documentElement.getAttribute("package");
130        final String target = getElementAttribute(documentElement,
131                                                  "instrumentation",
132                                                  "android:targetPackage");
133
134        String outputXmlFile = outputPathPrefix + ".xml";
135        final String xmlName = new File(outputPathPrefix).getName();
136        XMLGenerator xmlGenerator;
137        try {
138            xmlGenerator = new XMLGenerator(outputXmlFile) {
139                {
140                    Node testPackageElem = mDoc.getDocumentElement();
141
142                    setAttribute(testPackageElem, ATTRIBUTE_NAME, xmlName);
143                    setAttribute(testPackageElem, ATTRIBUTE_RUNNER, runner);
144                    setAttribute(testPackageElem, ATTRIBUTE_PACKAGE, packageName);
145                    setAttribute(testPackageElem, ATTRIBUTE_NS, packageName);
146                    setAttribute(testPackageElem, ATTRIBUTE_JAVA_PACKAGE_FILTER, javaPackageFilterArg);
147
148                    if (testType.type == TestType.HOST_SIDE_ONLY) {
149                        setAttribute(testPackageElem, ATTRIBUTE_HOST_SIDE_ONLY, "true");
150                        setAttribute(testPackageElem, ATTRIBUTE_JAR_PATH, testType.jarPath);
151                    }
152
153                    if (testType.type == TestType.VM_HOST_TEST) {
154                        setAttribute(testPackageElem, ATTRIBUTE_VM_HOST_TEST, "true");
155                        setAttribute(testPackageElem, ATTRIBUTE_JAR_PATH, testType.jarPath);
156                    }
157
158                    if (!packageName.equals(target)) {
159                        setAttribute(testPackageElem, ATTRIBUTE_TARGET, target);
160                        setAttribute(testPackageElem, ATTRIBUTE_TARGET_BINARY, target);
161                    }
162                }
163            };
164        } catch (ParserConfigurationException e) {
165            System.err.println("Can't initialize XML Generator " + outputXmlFile);
166            System.exit(1);
167            return;
168        }
169
170        ExpectationStore libcoreVogarExpectationStore;
171        ExpectationStore ctsVogarExpectationStore;
172
173        try {
174            libcoreVogarExpectationStore
175                    = VogarUtils.provideExpectationStore(libcoreExpectationDir);
176            ctsVogarExpectationStore = VogarUtils.provideExpectationStore(CTS_EXPECTATION_DIR);
177        } catch (IOException e) {
178            System.err.println("Can't initialize vogar expectation store from "
179                               + libcoreExpectationDir);
180            e.printStackTrace(System.err);
181            System.exit(1);
182            return;
183        }
184        ExpectationStore[] expectations = new ExpectationStore[] {
185            libcoreVogarExpectationStore, ctsVogarExpectationStore
186        };
187
188        JarFile jarFile = null;
189        try {
190            jarFile = new JarFile(jarFileName);
191        } catch (Exception e) {
192            System.err.println("cannot open jarfile " + jarFileName);
193            e.printStackTrace();
194            System.exit(1);
195        }
196
197        Map<String,TestClass> testCases = new LinkedHashMap<String, TestClass>();
198
199        Enumeration<JarEntry> jarEntries = jarFile.entries();
200        while (jarEntries.hasMoreElements()) {
201            JarEntry jarEntry = jarEntries.nextElement();
202            String name = jarEntry.getName();
203            if (!name.endsWith(".class")) {
204                continue;
205            }
206            String className
207                    = name.substring(0, name.length() - ".class".length()).replace('/', '.');
208
209            boolean matchesPrefix = false;
210            if (javaPackagePrefixes.length > 0) {
211                for (String javaPackagePrefix : javaPackagePrefixes) {
212                    if (className.startsWith(javaPackagePrefix)) {
213                        matchesPrefix = true;
214                    }
215                }
216            } else {
217                matchesPrefix = true;
218            }
219
220            if (!matchesPrefix) {
221                continue;
222            }
223
224            // Avoid inner classes: they should not have tests and often they can have dependencies
225            // on test frameworks that need to be resolved and would need to be on the classpath.
226            // e.g. Mockito.
227            if (className.contains("$")) {
228                continue;
229            }
230
231            try {
232                Class<?> klass = Class.forName(className,
233                                               false,
234                                               CollectAllTests.class.getClassLoader());
235                final int modifiers = klass.getModifiers();
236                if (Modifier.isAbstract(modifiers) || !Modifier.isPublic(modifiers)) {
237                    continue;
238                }
239
240                final boolean isJunit4Class = isJunit4Class(klass);
241                if (!isJunit4Class && !isJunit3Test(klass)) {
242                    continue;
243                }
244
245                try {
246                    klass.getConstructor(new Class<?>[] { String.class } );
247                    addToTests(expectations, architecture, testCases, klass);
248                    continue;
249                } catch (NoSuchMethodException e) {
250                } catch (SecurityException e) {
251                    System.out.println("Known bug (Working as intended): problem with class "
252                            + className);
253                    e.printStackTrace();
254                }
255
256                try {
257                    klass.getConstructor(new Class<?>[0]);
258                    addToTests(expectations, architecture, testCases, klass);
259                    continue;
260                } catch (NoSuchMethodException e) {
261                } catch (SecurityException e) {
262                    System.out.println("Known bug (Working as intended): problem with class "
263                            + className);
264                    e.printStackTrace();
265                }
266            } catch (ClassNotFoundException e) {
267                System.out.println("class not found " + className);
268                e.printStackTrace();
269                System.exit(1);
270            }
271        }
272
273        for (Iterator<TestClass> iterator = testCases.values().iterator(); iterator.hasNext();) {
274            TestClass type = iterator.next();
275            xmlGenerator.addTestClass(type);
276        }
277
278        try {
279            xmlGenerator.dump();
280        } catch (Exception e) {
281            System.err.println("cannot dump xml to " + outputXmlFile);
282            e.printStackTrace();
283            System.exit(1);
284        }
285    }
286
287    private static class TestType {
288        private static final int HOST_SIDE_ONLY = 1;
289        private static final int DEVICE_SIDE_ONLY = 2;
290        private static final int VM_HOST_TEST = 3;
291
292        private final int type;
293        private final String jarPath;
294
295        private TestType (int type, String jarPath) {
296            this.type = type;
297            this.jarPath = jarPath;
298        }
299
300        private static TestType getTestType(String makeFileName) {
301            if (makeFileName == null || makeFileName.isEmpty()) {
302                return new TestType(DEVICE_SIDE_ONLY, null);
303            }
304            int type = TestType.DEVICE_SIDE_ONLY;
305            String jarPath = null;
306            try {
307                BufferedReader reader = new BufferedReader(new FileReader(makeFileName));
308                String line;
309
310                while ((line = reader.readLine())!=null) {
311                    if (line.startsWith(TEST_TYPE)) {
312                        if (line.indexOf(ATTRIBUTE_VM_HOST_TEST) >= 0) {
313                            type = VM_HOST_TEST;
314                        } else {
315                            type = HOST_SIDE_ONLY;
316                        }
317                    } else if (line.startsWith(JAR_PATH)) {
318                        jarPath = line.substring(JAR_PATH.length(), line.length()).trim();
319                    }
320                }
321                reader.close();
322            } catch (IOException e) {
323            }
324            return new TestType(type, jarPath);
325        }
326    }
327
328    private static Element getElement(Element element, String tagName) {
329        NodeList elements = element.getElementsByTagName(tagName);
330        if (elements.getLength() > 0) {
331            return (Element) elements.item(0);
332        } else {
333            return null;
334        }
335    }
336
337    private static String getElementAttribute(Element element,
338                                              String elementName,
339                                              String attributeName) {
340        Element e = getElement(element, elementName);
341        if (e != null) {
342            return e.getAttribute(attributeName);
343        } else {
344            return "";
345        }
346    }
347
348    private static String getKnownFailure(final Class<?> testClass,
349            final String testName) {
350        return getAnnotation(testClass, testName, KNOWN_FAILURE);
351    }
352
353    private static boolean isKnownFailure(final Class<?> testClass,
354            final String testName) {
355        return getAnnotation(testClass, testName, KNOWN_FAILURE) != null;
356    }
357
358    private static boolean isSuppressed(final Class<?> testClass,
359            final String testName)  {
360        return getAnnotation(testClass, testName, SUPPRESSED_TEST) != null;
361    }
362
363    private static String getAnnotation(final Class<?> testClass,
364            final String testName, final String annotationName) {
365        try {
366            Method testMethod = testClass.getMethod(testName, (Class[])null);
367            Annotation[] annotations = testMethod.getAnnotations();
368            for (Annotation annot : annotations) {
369
370                if (annot.annotationType().getName().equals(annotationName)) {
371                    String annotStr = annot.toString();
372                    String knownFailure = null;
373                    if (annotStr.contains("(value=")) {
374                        knownFailure =
375                            annotStr.substring(annotStr.indexOf("=") + 1,
376                                    annotStr.length() - 1);
377
378                    }
379
380                    if (knownFailure == null) {
381                        knownFailure = "true";
382                    }
383
384                    return knownFailure;
385                }
386
387            }
388
389        } catch (NoSuchMethodException e) {
390        }
391
392        return null;
393    }
394
395    private static void addToTests(ExpectationStore[] expectations,
396                                   String architecture,
397                                   Map<String,TestClass> testCases,
398                                   Class<?> testClass) {
399        Set<String> testNames = new HashSet<String>();
400
401        boolean isJunit3Test = isJunit3Test(testClass);
402
403        Method[] testMethods = testClass.getMethods();
404        for (Method testMethod : testMethods) {
405            String testName = testMethod.getName();
406            if (testNames.contains(testName)) {
407                continue;
408            }
409
410            /* Make sure the method has the right signature. */
411            if (!Modifier.isPublic(testMethod.getModifiers())) {
412                continue;
413            }
414            if (!testMethod.getReturnType().equals(Void.TYPE)) {
415                continue;
416            }
417            if (testMethod.getParameterTypes().length != 0) {
418                continue;
419            }
420
421            if ((isJunit3Test && !testName.startsWith("test"))
422                    || (!isJunit3Test && !isJunit4TestMethod(testMethod))) {
423                continue;
424            }
425
426            testNames.add(testName);
427            addToTests(expectations, architecture, testCases, testClass, testName);
428        }
429    }
430
431    private static void addToTests(ExpectationStore[] expectations,
432                                   String architecture,
433                                   Map<String,TestClass> testCases,
434                                   Class<?> test,
435                                   String testName) {
436
437        String testClassName = test.getName();
438        String knownFailure = getKnownFailure(test, testName);
439
440        if (isKnownFailure(test, testName)) {
441            System.out.println("ignoring known failure: " + test + "#" + testName);
442            return;
443        } else if (isSuppressed(test, testName)) {
444            System.out.println("ignoring suppressed test: " + test + "#" + testName);
445            return;
446        } else if (VogarUtils.isVogarKnownFailure(expectations,
447                                                  testClassName,
448                                                  testName)) {
449            System.out.println("ignoring expectation known failure: " + test
450                               + "#" + testName);
451            return;
452        }
453
454        Set<String> supportedAbis = VogarUtils.extractSupportedAbis(architecture,
455                                                                    expectations,
456                                                                    testClassName,
457                                                                    testName);
458        int timeoutInMinutes = VogarUtils.timeoutInMinutes(expectations,
459                                                           testClassName,
460                                                           testName);
461        TestClass testClass;
462        if (testCases.containsKey(testClassName)) {
463            testClass = testCases.get(testClassName);
464        } else {
465            testClass = new TestClass(testClassName, new ArrayList<TestMethod>());
466            testCases.put(testClassName, testClass);
467        }
468
469        testClass.mCases.add(new TestMethod(testName, "", "", supportedAbis,
470              knownFailure, false, false, timeoutInMinutes));
471    }
472
473    private static boolean isJunit3Test(Class<?> klass) {
474        return TestCase.class.isAssignableFrom(klass);
475    }
476
477    private static boolean isJunit4Class(Class<?> klass) {
478        for (Annotation a : klass.getAnnotations()) {
479            if (RunWith.class.isAssignableFrom(a.annotationType())) {
480                // @RunWith is currently not supported for CTS tests because tradefed cannot handle
481                // a single test spawning other tests with different names.
482                System.out.println("Skipping test class " + klass.getName()
483                        + ": JUnit4 @RunWith is not supported");
484                return false;
485            }
486        }
487
488        for (Method m : klass.getMethods()) {
489            if (isJunit4TestMethod(m)) {
490                return true;
491            }
492        }
493
494        return false;
495    }
496
497    private static boolean isJunit4TestMethod(Method method) {
498        for (Annotation a : method.getAnnotations()) {
499            if (org.junit.Test.class.isAssignableFrom(a.annotationType())) {
500                return true;
501            }
502        }
503
504        return false;
505    }
506
507    /**
508     * Determines if a given string is a valid java package name
509     * @param javaPackageName
510     * @return true if it is valid, false otherwise
511     */
512    private static boolean isValidJavaPackage(String javaPackageName) {
513        String[] strSections = javaPackageName.split(".");
514        for (String strSection : strSections) {
515          if (!isValidJavaIdentifier(strSection)) {
516              return false;
517          }
518        }
519        return true;
520    }
521
522    /**
523     * Determines if a given string is a valid java identifier.
524     * @param javaIdentifier
525     * @return true if it is a valid identifier, false otherwise
526     */
527    private static boolean isValidJavaIdentifier(String javaIdentifier) {
528        if (javaIdentifier.length() == 0 ||
529                !Character.isJavaIdentifierStart(javaIdentifier.charAt(0))) {
530            return false;
531        }
532        for (int i = 1; i < javaIdentifier.length(); i++) {
533            if (!Character.isJavaIdentifierPart(javaIdentifier.charAt(i))) {
534                return false;
535            }
536        }
537        return true;
538    }
539}
540