1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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
17package com.android.ide.eclipse.adt.internal.launch;
18
19import com.android.SdkConstants;
20import com.android.ide.eclipse.adt.AdtConstants;
21import com.android.ide.eclipse.adt.AdtPlugin;
22
23import org.eclipse.core.runtime.CoreException;
24import org.eclipse.core.runtime.FileLocator;
25import org.eclipse.core.runtime.Platform;
26import org.eclipse.debug.core.ILaunchConfiguration;
27import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
28import org.osgi.framework.Bundle;
29
30import java.io.IOException;
31import java.net.URL;
32
33/**
34 * <p>
35 * For Android projects, android.jar gets added to the launch configuration of
36 * JUnit tests as a bootstrap entry. This breaks JUnit tests as android.jar
37 * contains a skeleton version of JUnit classes and the JVM will stop with an error similar
38 * to: <blockquote> Error occurred during initialization of VM
39 * java/lang/NoClassDefFoundError: java/lang/ref/FinalReference </blockquote>
40 * <p>
41 * At compile time, Eclipse does not know that there is no valid junit.jar in
42 * the classpath since it can find a correct reference to all the necessary
43 * org.junit.* classes in the android.jar so it does not prompt the user to add
44 * the JUnit3 or JUnit4 jar.
45 * <p>
46 * This delegates removes the android.jar from the bootstrap path and if
47 * necessary also puts back the junit.jar in the user classpath.
48 * <p>
49 * This delegate will be present for both Java and Android projects (delegates
50 * setting instead of only the current project) but the behavior for Java
51 * projects should be neutral since:
52 * <ol>
53 * <li>Java tests can only compile (and then run) when a valid junit.jar is
54 * present
55 * <li>There is no android.jar in Java projects
56 * </ol>
57 */
58public class JUnitLaunchConfigDelegate extends JUnitLaunchConfigurationDelegate {
59
60    private static final String JUNIT_JAR = "junit.jar"; //$NON-NLS-1$
61
62    @Override
63    public String[][] getBootpathExt(ILaunchConfiguration configuration) throws CoreException {
64        String[][] bootpath = super.getBootpathExt(configuration);
65        return fixBootpathExt(bootpath);
66    }
67
68    @Override
69    public String[] getClasspath(ILaunchConfiguration configuration) throws CoreException {
70        String[] classpath = super.getClasspath(configuration);
71        return fixClasspath(classpath, getJavaProjectName(configuration));
72    }
73
74    /**
75     * Removes the android.jar from the bootstrap path if present.
76     *
77     * @param bootpath Array of Arrays of bootstrap class paths
78     * @return a new modified (if applicable) bootpath
79     */
80    public static String[][] fixBootpathExt(String[][] bootpath) {
81        for (int i = 0; i < bootpath.length; i++) {
82            if (bootpath[i] != null && bootpath[i].length > 0) {
83                // we assume that the android.jar can only be present in the
84                // bootstrap path of android tests
85                if (bootpath[i][0].endsWith(SdkConstants.FN_FRAMEWORK_LIBRARY)) {
86                    bootpath[i] = null;
87                }
88            }
89        }
90        return bootpath;
91    }
92
93    /**
94     * Add the junit.jar to the user classpath; since Eclipse was relying on
95     * android.jar to provide the appropriate org.junit classes, it does not
96     * know it actually needs the junit.jar.
97     *
98     * @param classpath Array containing classpath
99     * @param projectName The name of the project (for logging purposes)
100     *
101     * @return a new modified (if applicable) classpath
102     */
103    public static String[] fixClasspath(String[] classpath, String projectName) {
104        // search for junit.jar; if any are found return immediately
105        for (int i = 0; i < classpath.length; i++) {
106            if (classpath[i].endsWith(JUNIT_JAR)) {
107                return classpath;
108            }
109        }
110
111        // This delegate being called without a junit.jar present is only
112        // possible for Android projects. In a non-Android project, the test
113        // would not compile and would be unable to run.
114        try {
115            // junit4 is backward compatible with junit3 and they uses the
116            // same junit.jar from bundle org.junit:
117            // When a project has mixed JUnit3 and JUnit4 tests, if JUnit3 jar
118            // is added first it is then replaced by the JUnit4 jar when user is
119            // prompted to fix the JUnit4 test failure
120            String jarLocation = getJunitJarLocation();
121            // we extend the classpath by one element and append junit.jar
122            String[] newClasspath = new String[classpath.length + 1];
123            System.arraycopy(classpath, 0, newClasspath, 0, classpath.length);
124            newClasspath[newClasspath.length - 1] = jarLocation;
125            classpath = newClasspath;
126        } catch (IOException e) {
127            // This should not happen as we depend on the org.junit
128            // plugin explicitly; the error is logged here so that the user can
129            // trace back the cause when the test fails to run
130            AdtPlugin.log(e, "Could not find a valid junit.jar");
131            AdtPlugin.printErrorToConsole(projectName,
132                    "Could not find a valid junit.jar");
133            // Return the classpath as-is (with no junit.jar) anyway because we
134            // will let the actual launch config fails.
135        }
136
137        return classpath;
138    }
139
140    /**
141     * Returns the path of the junit jar in the highest version bundle.
142     *
143     * (This is public only so that the test can call it)
144     *
145     * @return the path as a string
146     * @throws IOException
147     */
148    public static String getJunitJarLocation() throws IOException {
149        Bundle bundle = Platform.getBundle("org.junit"); //$NON-NLS-1$
150        if (bundle == null) {
151            throw new IOException("Cannot find org.junit bundle");
152        }
153        URL jarUrl = bundle.getEntry(AdtConstants.WS_SEP + JUNIT_JAR);
154        return FileLocator.resolve(jarUrl).getFile();
155    }
156}
157