/******************************************************************************* * Copyright Technophobia Ltd 2012 * * This file is part of the Substeps Eclipse Plugin. * * The Substeps Eclipse Plugin is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License v1.0. * * The Substeps Eclipse Plugin is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * Eclipse Public License for more details. * * You should have received a copy of the Eclipse Public License * along with the Substeps Eclipse Plugin. If not, see <http://www.eclipse.org/legal/epl-v10.html>. ******************************************************************************/ package com.technophobia.substeps.junit.launcher; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMember; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.launching.AbstractJavaLaunchConfigurationDelegate; import org.eclipse.jdt.launching.ExecutionArguments; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.IVMRunner; import org.eclipse.jdt.launching.SocketUtil; import org.eclipse.jdt.launching.VMRunnerConfiguration; import org.osgi.framework.Bundle; import com.technophobia.eclipse.launcher.config.SubstepsLaunchConfigurationConstants; import com.technophobia.substeps.FeatureEditorPlugin; import com.technophobia.substeps.FeatureRunnerPlugin; import com.technophobia.substeps.junit.launcher.config.SubstepsLaunchConfigWorkingCopyDecorator; import com.technophobia.substeps.junit.ui.SubstepsFeatureMessages; import com.technophobia.substeps.runner.RemoteTestRunner; import com.technophobia.substeps.supplier.Callback1; import com.technophobia.substeps.supplier.Predicate; import com.technophobia.substeps.util.ModelOperation; import com.technophobia.substeps.util.TemporaryModelEnhancer; public class SubstepsLaunchConfigurationDelegate extends AbstractJavaLaunchConfigurationDelegate { private boolean keepAlive = false; private int port; private IMember[] testElements; /* * (non-Javadoc) * * @see * org.eclipse.debug.core.model.ILaunchConfigurationDelegate#launch(org. * eclipse.debug.core.ILaunchConfiguration, java.lang.String, * org.eclipse.debug.core.ILaunch, * org.eclipse.core.runtime.IProgressMonitor) */ @Override public synchronized void launch(final ILaunchConfiguration configuration, final String m, final ILaunch launch, final IProgressMonitor mtr) throws CoreException { final IProgressMonitor monitor; if (mtr == null) { monitor = new NullProgressMonitor(); } else { monitor = mtr; } final String mode; if (m.equals(SubstepsLaunchConfigurationConstants.MODE_RUN_QUIETLY_MODE)) { launch.setAttribute(SubstepsLaunchConfigurationConstants.ATTR_NO_DISPLAY, "true"); //$NON-NLS-1$ mode = ILaunchManager.RUN_MODE; } else { mode = m; } monitor.beginTask(MessageFormat.format("{0}...", configuration.getName()), 5); //$NON-NLS-1$ // check for cancellation if (monitor.isCanceled()) { return; } try { monitor.subTask(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_verifying_attributes_description); try { preLaunchCheck(configuration, launch, new SubProgressMonitor(monitor, 2)); } catch (final CoreException e) { if (e.getStatus().getSeverity() == IStatus.CANCEL) { monitor.setCanceled(true); return; } throw e; } // check for cancellation if (monitor.isCanceled()) { return; } final TemporaryModelEnhancer<IJavaProject> modelEnhancer = new TemporaryModelEnhancer<IJavaProject>( addSubstepsToClasspath(), removeSubstepsFromClasspath(), doRunTests(mode, configuration, launch, monitor), isSubstepsNotOnClasspath(configuration)); modelEnhancer.doOperationFor(getJavaProject(configuration)); } finally { testElements = null; monitor.done(); } } private Callback1<IJavaProject> addSubstepsToClasspath() { return new Callback1<IJavaProject>() { @Override public void doCallback(final IJavaProject project) { try { final List<IClasspathEntry> newClasspath = new ArrayList<IClasspathEntry>(Arrays.asList(project .getRawClasspath())); final List<String> jarFiles = new SubstepJarProvider().junitRunnerJars(); for (final String jarFile : jarFiles) { newClasspath.add(JavaCore.newLibraryEntry(new Path(jarFile), null, null)); } project.setRawClasspath(newClasspath.toArray(new IClasspathEntry[newClasspath.size()]), null); } catch (final JavaModelException ex) { FeatureRunnerPlugin.error("Could not add substeps jars to classpath", ex); } } }; } private Callback1<IJavaProject> removeSubstepsFromClasspath() { return new Callback1<IJavaProject>() { @Override public void doCallback(final IJavaProject project) { try { final List<IClasspathEntry> newClasspath = new ArrayList<IClasspathEntry>(Arrays.asList(project .getRawClasspath())); final List<String> jarFiles = new SubstepJarProvider().junitRunnerJars(); for (final String jarFile : jarFiles) { newClasspath.remove(JavaCore.newLibraryEntry(new Path(jarFile), null, null)); } project.setRawClasspath(newClasspath.toArray(new IClasspathEntry[newClasspath.size()]), null); } catch (final JavaModelException ex) { FeatureRunnerPlugin.error("Could not remove substeps jars from classpath", ex); } } }; } private ModelOperation<IJavaProject> doRunTests(final String mode, final ILaunchConfiguration configuration, final ILaunch launch, final IProgressMonitor monitor) { return new ModelOperation<IJavaProject>() { @Override public void doOperationOn(final IJavaProject t) throws CoreException { keepAlive = mode.equals(ILaunchManager.DEBUG_MODE) && configuration.getAttribute(SubstepsLaunchConfigurationConstants.ATTR_KEEPRUNNING, false); port = evaluatePort(); launch.setAttribute(SubstepsLaunchConfigurationConstants.ATTR_PORT, String.valueOf(port)); testElements = evaluateTests(configuration, new SubProgressMonitor(monitor, 1)); final String mainTypeName = verifyMainTypeName(configuration); final IVMRunner runner = getVMRunner(configuration, mode); final File workingDir = verifyWorkingDirectory(configuration); String workingDirName = null; if (workingDir != null) { workingDirName = workingDir.getAbsolutePath(); } // Environment variables final String[] envp = getEnvironment(configuration); final ArrayList<String> vmArguments = new ArrayList<String>(); final ArrayList<String> programArguments = new ArrayList<String>(); collectExecutionArguments(configuration, vmArguments, programArguments); // VM-specific attributes final Map<String, Object> vmAttributesMap = getVMSpecificAttributesMap(configuration); // Classpath final String[] classpath = getClasspath(configuration); // Create VM config final VMRunnerConfiguration runConfig = new VMRunnerConfiguration(mainTypeName, classpath); runConfig.setVMArguments(vmArguments.toArray(new String[vmArguments.size()])); runConfig.setProgramArguments(programArguments.toArray(new String[programArguments.size()])); runConfig.setEnvironment(envp); runConfig.setWorkingDirectory(workingDirName); runConfig.setVMSpecificAttributesMap(vmAttributesMap); // Bootpath runConfig.setBootClassPath(getBootpath(configuration)); // check for cancellation if (monitor.isCanceled()) { return; } // done the verification phase monitor.worked(1); monitor.subTask(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_create_source_locator_description); // set the default source locator if required setDefaultSourceLocator(launch, configuration); monitor.worked(1); // Launch the configuration - 1 unit of work runner.run(runConfig, launch, monitor); // check for cancellation if (monitor.isCanceled()) { return; } } }; } private Predicate<IJavaProject> isSubstepsNotOnClasspath(final ILaunchConfiguration configuration) { return new Predicate<IJavaProject>() { @Override public boolean forModel(final IJavaProject project) { try { final IJavaElement element = getMainElementFromProject(configuration, project); return element == null; } catch (final JavaModelException ex) { FeatureRunnerPlugin.error("Could not determine if substeps runner jars were on the classpath", ex); } catch (final CoreException ex) { FeatureRunnerPlugin.error("Could not determine if substeps runner jars were on the classpath", ex); } return true; } }; } private int evaluatePort() throws CoreException { final int p = SocketUtil.findFreePort(); if (p == -1) { abort(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_error_no_socket, null, IJavaLaunchConfigurationConstants.ERR_NO_SOCKET_AVAILABLE); } return p; } /** * Performs a check on the launch configuration's attributes. If an * attribute contains an invalid value, a {@link CoreException} with the * error is thrown. * * @param configuration * the launch configuration to verify * @param launch * the launch to verify * @param monitor * the progress monitor to use * @throws CoreException * an exception is thrown when the verification fails */ protected void preLaunchCheck(final ILaunchConfiguration configuration, final ILaunch launch, final IProgressMonitor monitor) throws CoreException { try { final IJavaProject javaProject = getJavaProject(configuration); if ((javaProject == null) || !javaProject.exists()) { abort(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_error_invalidproject, null, IJavaLaunchConfigurationConstants.ERR_NOT_A_JAVA_PROJECT); } } finally { monitor.done(); } } /* * (non-Javadoc) * * @see org.eclipse.jdt.launching.AbstractJavaLaunchConfigurationDelegate# * verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration) */ @Override public String verifyMainTypeName(final ILaunchConfiguration configuration) throws CoreException { return RemoteTestRunner.class.getName(); } /** * Evaluates all test elements selected by the given launch configuration. * The elements are of type {@link IType} or {@link IMethod}. At the moment * it is only possible to run a single method or a set of types, but not * mixed or more than one method at a time. * * @param configuration * the launch configuration to inspect * @param monitor * the progress monitor * @return returns all types or methods that should be ran * @throws CoreException * an exception is thrown when the search for tests failed */ protected IMember[] evaluateTests(final ILaunchConfiguration configuration, final IProgressMonitor monitor) throws CoreException { final IJavaProject javaProject = getJavaProject(configuration); final IJavaElement testTarget = getTestTarget(configuration, javaProject); final String testMethodName = configuration.getAttribute( SubstepsLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$ if (testMethodName.length() > 0) { if (testTarget instanceof IType) { return new IMember[] { ((IType) testTarget).getMethod(testMethodName, new String[0]) }; } } final HashSet<IMember> result = new HashSet<IMember>(); result.add(testTarget.getJavaProject().findType(SubstepsLaunchConfigWorkingCopyDecorator.FEATURE_TEST)); return result.toArray(new IMember[result.size()]); } /** * Collects all VM and program arguments. Implementors can modify and add * arguments. * * @param configuration * the configuration to collect the arguments for * @param vmArguments * a {@link List} of {@link String} representing the resulting VM * arguments * @param programArguments * a {@link List} of {@link String} representing the resulting * program arguments * @exception CoreException * if unable to collect the execution arguments */ protected void collectExecutionArguments(final ILaunchConfiguration configuration, final List<String> vmArguments, final List<String> programArguments) throws CoreException { // add program & VM arguments provided by getProgramArguments and // getVMArguments final String pgmArgs = getProgramArguments(configuration); final String vmArgs = getVMArguments(configuration); final ExecutionArguments execArgs = new ExecutionArguments(vmArgs, pgmArgs); vmArguments.addAll(Arrays.asList(execArgs.getVMArgumentsArray())); vmArguments.addAll(substepsVMArguments(configuration)); programArguments.addAll(Arrays.asList(execArgs.getProgramArgumentsArray())); final String testFailureNames = configuration.getAttribute( SubstepsLaunchConfigurationConstants.ATTR_FAILURES_NAMES, ""); //$NON-NLS-1$ programArguments.add("version=3"); programArguments.add("port=" + String.valueOf(port)); if (keepAlive) programArguments.add("keepalive"); //$NON-NLS-1$ // final String testRunnerKind = getTestRunnerKind(configuration); // programArguments.add("-testLoaderClass"); //$NON-NLS-1$ // programArguments.add(testRunnerKind.getLoaderClassName()); // programArguments.add("-loaderpluginname"); //$NON-NLS-1$ // programArguments.add(testRunnerKind.getLoaderPluginId()); final IMember[] elements = this.testElements; // a test name was specified just run the single test if (elements.length == 1) { if (elements[0] instanceof IMethod) { final IMethod method = (IMethod) elements[0]; programArguments.add("test=" + method.getDeclaringType().getFullyQualifiedName() + ':' + method.getElementName()); } else if (elements[0] instanceof IType) { final IType type = (IType) elements[0]; programArguments.add("classNames=" + type.getFullyQualifiedName()); } else { abort(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_error_wrong_input, null, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE); } } else if (elements.length > 1) { final String fileName = createTestNamesFile(elements); programArguments.add("testNameFile=" + fileName); } if (testFailureNames.length() > 0) { programArguments.add("testfailures=" + testFailureNames); } } private Collection<String> substepsVMArguments(final ILaunchConfiguration configuration) { final Collection<String> results = new ArrayList<String>(); final IProject project = projectFromConfig(configuration); if (project != null) { results.add("-DsubstepsFeatureFile=" + project.getLocation().addTrailingSeparator() .append(getConfigAttribute(configuration, SubstepsFeatureLaunchShortcut.ATTR_FEATURE_FILE)) .toOSString()); results.add("-DsubstepsFile=" + project .getLocation() .addTrailingSeparator() .append(getConfigAttribute(configuration, SubstepsLaunchConfigurationConstants.ATTR_SUBSTEPS_FILE)).toOSString()); final Collection<String> stepImplementationClasses = FeatureEditorPlugin.instance() .getStepImplementationProvider().stepImplementationClasses(project); results.add("-DsubstepsImplClasses=" + createStringFrom(stepImplementationClasses)); results.add("-DsubstepsTags=--unimplemented"); try { results.add("-DoutputFolder=" + getJavaProject(configuration).getOutputLocation().removeFirstSegments(1).toOSString()); } catch (final JavaModelException e) { FeatureRunnerPlugin.log(e); } catch (final CoreException e) { FeatureRunnerPlugin.log(e); } // results.add(getConfigAttribute(configuration, // IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS)); } return results; } private IProject projectFromConfig(final ILaunchConfiguration configuration) { try { return getJavaProject(configuration).getProject(); } catch (final CoreException e) { FeatureRunnerPlugin.log(e); return null; } } private String getConfigAttribute(final ILaunchConfiguration configuration, final String configName) { try { return configuration.getAttribute(configName, ""); } catch (final CoreException e) { FeatureRunnerPlugin.log(e); return ""; } } private String createTestNamesFile(final IMember[] elements) throws CoreException { try { final File file = File.createTempFile("testNames", ".txt"); //$NON-NLS-1$ //$NON-NLS-2$ file.deleteOnExit(); BufferedWriter bw = null; try { bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")); //$NON-NLS-1$ for (int i = 0; i < elements.length; i++) { if (elements[i] instanceof IType) { final IType type = (IType) elements[i]; final String testName = type.getFullyQualifiedName(); bw.write(testName); bw.newLine(); } else { abort(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_error_wrong_input, null, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE); } } } finally { if (bw != null) { bw.close(); } } return file.getAbsolutePath(); } catch (final IOException e) { throw new CoreException(new Status(IStatus.ERROR, FeatureRunnerPlugin.PLUGIN_ID, IStatus.ERROR, "", e)); //$NON-NLS-1$ } } /* * (non-Javadoc) * * @see org.eclipse.jdt.launching.AbstractJavaLaunchConfigurationDelegate# * getClasspath(org.eclipse.debug.core.ILaunchConfiguration) */ @Override public String[] getClasspath(final ILaunchConfiguration configuration) throws CoreException { final String[] cp = super.getClasspath(configuration); Set<String> total = new LinkedHashSet<String>(); // TODO might be a good idea to log out the classpath that we're firing the tests off with..?? for (String cpElem : cp){ total.add(cpElem); } // This is the classpath to the test launcher plugin - this works whether in eclipse or tycho // and hopefully in a deployed plugin String thisPluginsClasspath = getPluginClasspath(FeatureRunnerPlugin.PLUGIN_ID); final List<String> junitEntries = new SubstepJarProvider().allSubstepJars(); total.add(thisPluginsClasspath); total.addAll(junitEntries); return total.toArray(new String[total.size()]); } private String getPluginClasspath(String pluginId) { String finalUrl = null; final Bundle bundle = FeatureRunnerPlugin.instance().getBundle(pluginId); URL url = null; if (Platform.inDevelopmentMode()) { url = bundle.getEntry("target/classes"); } if (url == null){ url = bundle.getEntry("/"); } Assert.isNotNull(url, "url for plugin can't be null"); try { finalUrl = FileLocator.toFileURL(url).getFile(); } catch (IOException e) { e.printStackTrace(); } return finalUrl; } private final IJavaElement getTestTarget(final ILaunchConfiguration configuration, final IJavaProject javaProject) throws CoreException { final String containerHandle = configuration.getAttribute( SubstepsLaunchConfigurationConstants.ATTR_TEST_CONTAINER, ""); //$NON-NLS-1$ if (containerHandle.length() != 0) { final IJavaElement element = JavaCore.create(containerHandle); if (element == null || !element.exists()) { abort(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_error_input_element_deosn_not_exist, null, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE); } return element; } final IJavaElement element = getMainElementFromProject(configuration, javaProject); if (element != null) { return element; } abort(SubstepsFeatureMessages.SubstepsLaunchConfigurationDelegate_input_type_does_not_exist, null, IJavaLaunchConfigurationConstants.ERR_UNSPECIFIED_MAIN_TYPE); return null; // not reachable } private IJavaElement getMainElementFromProject(final ILaunchConfiguration configuration, final IJavaProject javaProject) throws CoreException, JavaModelException { final String testTypeName = getMainTypeName(configuration); if (testTypeName != null && testTypeName.length() != 0) { final IType type = javaProject.findType(testTypeName); if (type != null && type.exists()) { return type; } } return null; } /* * (non-Javadoc) * * @see * org.eclipse.jdt.internal.junit.launcher.ITestFindingAbortHandler#abort * (java.lang.String, java.lang.Throwable, int) */ @Override protected void abort(final String message, final Throwable exception, final int code) throws CoreException { throw new CoreException(new Status(IStatus.ERROR, FeatureRunnerPlugin.PLUGIN_ID, code, message, exception)); } private String createStringFrom(final Collection<String> collection) { final StringBuilder sb = new StringBuilder(); if (collection != null) { for (final String stepImpl : collection) { sb.append(stepImpl); sb.append(";"); } } return sb.toString(); } }