package org.xtest.ui.runner; import static com.google.common.collect.Sets.newHashSet; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.TimeUnit; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.JavaCore; import org.eclipse.xtext.resource.XtextResourceSet; import org.eclipse.xtext.util.CancelIndicator; import org.xtest.RunType; import org.xtest.XTestRunner; import org.xtest.interpreter.XTestInterpreter; import org.xtest.results.XTestResult; import org.xtest.runner.external.DependencyAcceptor; import org.xtest.runner.util.ClasspathUtils; import org.xtest.ui.mediator.ValidationStartedEvent; import org.xtest.ui.resource.XtestDependencyAcceptingResource; import org.xtest.xTest.Body; import org.xtest.xTest.impl.BodyImplCustom; import com.google.common.base.Optional; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.eventbus.EventBus; import com.google.inject.Inject; import com.google.inject.Provider; /** * Custom implementation of XTestRunner for use in the UI that overrides the classloader of the * xtest interpreter with one that recognizes classes in the runtime java project * * @author Michael Barry */ public class UiXTestRunner extends XTestRunner { /** * Time to wait between checking the caller's cancel indicator. */ private static final long DELAY_BETWEEN_CHECKS = 10; @Inject private EventBus eventBus; @Inject private Provider<XTestInterpreter> interpreterProvider; @Override public XTestResult run(final Body main, RunType weight, CancelIndicator monitor) { eventBus.post(new ValidationStartedEvent(main.eResource().getURI())); XTestResult result; if (weight == RunType.LIGHTWEIGHT) { result = runInSeparateThread(main, weight, monitor); } else { result = runInThisThread(main, weight, monitor); } return result; } @SuppressWarnings("restriction") @Override protected XTestInterpreter getInterpreter(Resource resource) { // BORROWED FROM // org.eclipse.xtext.purexbase.ui.autoedit.ReplAutoEdit.java // in order to use the classloader of the java project in the running // instance of eclipse rather than the classloader of this class itself XTestInterpreter interpreter = interpreterProvider.get(); if (resource instanceof XtestDependencyAcceptingResource) { XtestDependencyAcceptingResource xtestResource = (XtestDependencyAcceptingResource) resource; Optional<DependencyAcceptor> acceptor = xtestResource.getAcceptor(); ResourceSet set = resource.getResourceSet(); ClassLoader cl = getClass().getClassLoader(); if (set instanceof XtextResourceSet) { Object context = ((XtextResourceSet) set).getClasspathURIContext(); if (context instanceof IJavaProject) { try { final IJavaProject jp = (IJavaProject) context; ArrayList<IClasspathEntry> classpath = Lists.newArrayList(jp .getResolvedClasspath(true)); Set<IPath> visited = Sets.newHashSet(); final IWorkspaceRoot root = jp.getProject().getWorkspace().getRoot(); Set<URL> urls = newHashSet(); for (int i = 0; i < classpath.size(); i++) { final IClasspathEntry entry = classpath.get(i); // Avoid re-visiting entries in case there is a circular project // dependency if (!visited.contains(entry.getPath())) { visited.add(entry.getPath()); if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) { IPath outputLocation = entry.getOutputLocation(); if (outputLocation == null) { outputLocation = jp.getOutputLocation(); } IFolder folder = root.getFolder(outputLocation); if (folder.exists()) { urls.add(new URL(folder.getRawLocationURI().toASCIIString() + "/")); } } else if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) { IPath outputLocation = entry.getOutputLocation(); IProject project = jp.getProject().getWorkspace().getRoot() .getProject(entry.getPath().toString()); if (outputLocation == null) { // Modified from getContainerForLocation to getProject // because // on my setup, getContainerForLocation was returning null. // Also // added null checks for safety if (project == null) { project = (IProject) jp.getProject().getWorkspace() .getRoot() .getContainerForLocation(entry.getPath()); } if (project != null) { IJavaProject javaProject = JavaCore.create(project); if (javaProject.exists()) { outputLocation = javaProject.getOutputLocation(); } } } // Added null check for safety if (outputLocation != null) { IFolder folder = root.getFolder(outputLocation); if (folder.exists()) { urls.add(new URL(folder.getRawLocationURI() .toASCIIString() + "/")); } } if (project != null) { IClasspathEntry[] resolvedClasspath = JavaCore.create( project).getResolvedClasspath(true); classpath.addAll(Lists.newArrayList(resolvedClasspath)); } } else { IPath path = entry.getPath(); path = ClasspathUtils.normalizePath(root, path); urls.add(path.toFile().toURI().toURL()); } } } URL[] array = urls.toArray(new URL[urls.size()]); if (acceptor.isPresent()) { cl = new RecordingClassLoader(array, acceptor.get()); } else { cl = new URLClassLoader(array); } } catch (Exception e) { e.printStackTrace(); } } } interpreter.setClassLoader(cl); } return interpreter; } private XTestResult runInSeparateThread(final Body main, RunType weight, CancelIndicator monitor) { XTestResult result; String name = "Running " + ((BodyImplCustom) main).getFileName(); ArrayBlockingQueue<XTestResult> resultQueue = new ArrayBlockingQueue<XTestResult>(1); TestRunnerJob job = new TestRunnerJob(name, resultQueue, main); job.schedule(); XTestResult jobResult = UiXTestRunner.super.run(main, weight, monitor); // TODO should be able to specify a maximum allowed time for tests to run and cancel // after // that while (jobResult == null) { try { jobResult = resultQueue.poll(DELAY_BETWEEN_CHECKS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } if (monitor.isCanceled()) { job.cancel(); break; } } result = jobResult == null ? new XTestResult(main) : jobResult; return result; } private XTestResult runInThisThread(final Body main, RunType weight, CancelIndicator monitor) { XTestResult result = UiXTestRunner.super.run(main, weight, monitor); return result; } /** * {@link CancelIndicator} that delegates to an {@link IProgressMonitor} * * @author Michael Barry */ private static class ProgressMonitorCancelIndicator implements CancelIndicator { private final IProgressMonitor monitor; private ProgressMonitorCancelIndicator(IProgressMonitor monitor) { this.monitor = monitor; } @Override public boolean isCanceled() { return monitor.isCanceled(); } } private static class RecordingClassLoader extends URLClassLoader { private final DependencyAcceptor acceptor; public RecordingClassLoader(URL[] array, DependencyAcceptor acceptor) { super(array); this.acceptor = acceptor; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> loadClass = super.findClass(name); String path = name.replace('.', '/').concat(".class"); URL res = findResource(path); if (res != null) { try { URI uri = res.toURI(); acceptor.accept(uri); } catch (URISyntaxException e) { } } return loadClass; } } /** * Test runner job * * @author Michael Barry */ private class TestRunnerJob extends Job { private final Body main; private final ArrayBlockingQueue<XTestResult> result; private TestRunnerJob(String name, ArrayBlockingQueue<XTestResult> result, Body main) { super(name); this.result = result; this.main = main; } @Override protected void canceling() { super.canceling(); getThread().interrupt(); } @Override protected IStatus run(final IProgressMonitor arg0) { XTestResult xtestResult = new XTestResult(main); try { CancelIndicator indicator = new ProgressMonitorCancelIndicator(arg0); xtestResult = UiXTestRunner.super.run(main, RunType.LIGHTWEIGHT, indicator); } finally { result.offer(xtestResult); } return Status.OK_STATUS; } } }