blob: 46eaa76233cf45846bc5c75160c441ea4cb4a511 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.dltk.internal.testing.model;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchListener;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.dltk.core.IScriptProject;
import org.eclipse.dltk.internal.testing.Messages;
import org.eclipse.dltk.internal.testing.ui.DLTKTestingPreferencesConstants;
import org.eclipse.dltk.internal.testing.ui.TestRunnerViewPart;
import org.eclipse.dltk.launching.ScriptLaunchConfigurationConstants;
import org.eclipse.dltk.testing.DLTKTestingConstants;
import org.eclipse.dltk.testing.DLTKTestingPlugin;
import org.eclipse.dltk.testing.ITestSession;
import org.eclipse.dltk.testing.model.ITestRunSession;
import org.eclipse.dltk.testing.model.ITestingModel;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PartInitException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/**
* Central registry for JUnit test runs.
*/
public final class DLTKTestingModel implements ITestingModel {
private final class DLTKTestingLaunchListener implements ILaunchListener {
/**
* Used to track new launches. We need to do this so that we only attach
* a TestRunner once to a launch. Once a test runner is connected, it is
* removed from the set.
*/
private final HashSet<ILaunch> fTrackedLaunches = new HashSet<>(20);
protected void initialize(ILaunchManager launchManager) {
fTrackedLaunches.clear();
ILaunch[] launchs = launchManager.getLaunches();
for (int i = 0, size = launchs.length; i < size; ++i) {
fTrackedLaunches.add(launchs[i]);
}
}
@Override
public void launchAdded(ILaunch launch) {
fTrackedLaunches.add(launch);
}
@Override
public void launchRemoved(final ILaunch launch) {
fTrackedLaunches.remove(launch);
}
@Override
public void launchChanged(final ILaunch launch) {
if (!fTrackedLaunches.contains(launch))
return;
ILaunchConfiguration config = launch.getLaunchConfiguration();
if (config == null)
return;
final IScriptProject javaProject = DLTKTestingConstants
.getScriptProject(config);
if (javaProject == null)
return;
// test whether the launch defines the JUnit attributes
String portStr = launch
.getAttribute(DLTKTestingConstants.ATTR_PORT);
if (portStr != null) {
try {
final int port = Integer.parseInt(portStr);
fTrackedLaunches.remove(launch);
getDisplay().asyncExec(
() -> connectTestRunner(launch, javaProject, port));
} catch (NumberFormatException e) {
return;
}
} else {
String atr = launch
.getAttribute(DLTKTestingConstants.LAUNCH_ATTR_KEY);
if (atr != null) {
fTrackedLaunches.remove(launch);
getDisplay().asyncExec(
() -> connectTestRunner(launch, javaProject));
}
}
}
private void connectTestRunner(ILaunch launch,
IScriptProject javaProject) {
showTestRunnerViewPartInActivePage();
limitSessionHistory();
addTestRunSession(new TestRunSession(launch, javaProject,
new RemoteTestRunnerClient()));
}
private void connectTestRunner(ILaunch launch,
IScriptProject javaProject, int port) {
showTestRunnerViewPartInActivePage();
limitSessionHistory();
addTestRunSession(new TestRunSession(launch, javaProject,
new SocketTestRunnerClient(port)));
}
private void limitSessionHistory() {
// TODO: Do notifications have to be sent in UI thread?
// Check concurrent access to fTestRunSessions (no problem inside
// asyncExec())
int maxCount = DLTKTestingPlugin.getDefault().getPreferenceStore()
.getInt(DLTKTestingPreferencesConstants.MAX_TEST_RUNS);
synchronized (fTestRunSessions) {
int toDelete = fTestRunSessions.size() - maxCount;
while (toDelete > 0) {
toDelete--;
TestRunSession session = fTestRunSessions.removeLast();
notifyTestRunSessionRemoved(session);
}
}
}
private Display getDisplay() {
// Shell shell= getActiveWorkbenchShell();
// if (shell != null) {
// return shell.getDisplay();
// }
Display display = Display.getCurrent();
if (display == null) {
display = Display.getDefault();
}
return display;
}
}
public TestRunnerViewPart showTestRunnerViewPartInActivePage() {
return showTestRunnerViewPartInActivePage(
findTestRunnerViewPartInActivePage());
}
private TestRunnerViewPart showTestRunnerViewPartInActivePage(
TestRunnerViewPart testRunner) {
IWorkbenchPart activePart = null;
IWorkbenchPage page = null;
try {
// TODO: have to force the creation of view part contents
// otherwise the UI will not be updated
if (testRunner != null && testRunner.isCreated())
return testRunner;
page = DLTKTestingPlugin.getActivePage();
if (page == null)
return null;
activePart = page.getActivePart();
// show the result view if it isn't shown yet
return (TestRunnerViewPart) page.showView(TestRunnerViewPart.NAME);
} catch (PartInitException pie) {
DLTKTestingPlugin.log(pie);
return null;
} finally {
// restore focus stolen by the creation of the result view
if (page != null && activePart != null)
page.activate(activePart);
}
}
private TestRunnerViewPart findTestRunnerViewPartInActivePage() {
IWorkbenchPage page = DLTKTestingPlugin.getActivePage();
if (page == null)
return null;
return (TestRunnerViewPart) page.findView(TestRunnerViewPart.NAME);
}
public static IScriptProject getScriptProject(
ILaunchConfiguration configuration) {
try {
String projectName = configuration.getAttribute(
ScriptLaunchConfigurationConstants.ATTR_PROJECT_NAME,
(String) null);
if (projectName != null && projectName.length() > 0) {
return DLTKCore.create(ResourcesPlugin.getWorkspace().getRoot()
.getProject(projectName));
}
} catch (CoreException e) {
}
return null;
}
private final ListenerList<ITestRunSessionListener> fTestRunSessionListeners = new ListenerList<>();
/**
* Active test run sessions, youngest first.
*/
private final LinkedList<TestRunSession> fTestRunSessions = new LinkedList<>();
private final DLTKTestingLaunchListener fLaunchListener = new DLTKTestingLaunchListener();
private boolean started = false;
/**
* Starts the model (called by the {@link DLTKTestingPlugin} on startup).
*/
@Override
public synchronized void start() {
if (started) {
return;
}
started = true;
ILaunchManager launchManager = DebugPlugin.getDefault()
.getLaunchManager();
launchManager.addLaunchListener(fLaunchListener);
fLaunchListener.initialize(launchManager);
/*
* TODO: restore on restart: - only import headers! - only import last n
* sessions; remove all other files in historyDirectory
*/
// File historyDirectory= JUnitPlugin.getHistoryDirectory();
// File[] swapFiles= historyDirectory.listFiles();
// if (swapFiles != null) {
// Arrays.sort(swapFiles, new Comparator() {
// public int compare(Object o1, Object o2) {
// String name1= ((File) o1).getName();
// String name2= ((File) o2).getName();
// return name1.compareTo(name2);
// }
// });
// for (int i= 0; i < swapFiles.length; i++) {
// final File file= swapFiles[i];
// SafeRunner.run(new ISafeRunnable() {
// public void run() throws Exception {
// importTestRunSession(file );
// }
// public void handleException(Throwable exception) {
// JUnitPlugin.log(exception);
// }
// });
// }
// }
}
/**
* Stops the model (called by the {@link DLTKTestingPlugin} on shutdown).
*/
public void stop() {
ILaunchManager launchManager = DebugPlugin.getDefault()
.getLaunchManager();
launchManager.removeLaunchListener(fLaunchListener);
File historyDirectory = DLTKTestingPlugin.getHistoryDirectory();
File[] swapFiles = historyDirectory.listFiles();
if (swapFiles != null) {
for (int i = 0; i < swapFiles.length; i++) {
swapFiles[i].delete();
}
}
started = false;
}
@Override
public void addTestRunSessionListener(ITestRunSessionListener listener) {
fTestRunSessionListeners.add(listener);
}
@Override
public void removeTestRunSessionListener(ITestRunSessionListener listener) {
fTestRunSessionListeners.remove(listener);
}
/**
* @return a list of active {@link TestRunSession}s. The list is a copy of
* the internal data structure and modifications do not affect the
* global list of active sessions. The list is sorted by age,
* youngest first.
*/
@Override
public List<TestRunSession> getTestRunSessions() {
synchronized (fTestRunSessions) {
return new ArrayList<>(fTestRunSessions);
}
}
@Override
public ITestRunSession getTestRunSession(ILaunch launch) {
Assert.isNotNull(launch);
synchronized (fTestRunSessions) {
for (final TestRunSession session : fTestRunSessions) {
if (launch.equals(session.getLaunch())) {
return session;
}
}
}
return null;
}
/**
* Adds the given {@link TestRunSession} and notifies all registered
* {@link ITestRunSessionListener}s.
* <p>
* <b>To be called in the UI thread only!</b>
* </p>
*
* @param testRunSession
* the session to add
*/
@Override
public void addTestRunSession(TestRunSession testRunSession) {
Assert.isNotNull(testRunSession);
synchronized (fTestRunSessions) {
Assert.isLegal(!fTestRunSessions.contains(testRunSession));
fTestRunSessions.addFirst(testRunSession);
}
notifyTestRunSessionAdded(testRunSession);
}
/**
* Imports a test run session from the given file.
*
* @param file
* a file containing a test run session transcript
* @return the imported test run session
* @throws CoreException
* if the import failed
*/
public static ITestSession importTestRunSession(File file)
throws CoreException {
try {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
// parserFactory.setValidating(true); // TODO: add DTD and debug
// flag
SAXParser parser = parserFactory.newSAXParser();
TestRunHandler handler = new TestRunHandler();
parser.parse(file, handler);
TestRunSession session = handler.getTestRunSession();
DLTKTestingPlugin.getModel().addTestRunSession(session);
return session;
} catch (ParserConfigurationException e) {
throwImportError(file, e);
} catch (SAXException e) {
throwImportError(file, e);
} catch (IOException e) {
throwImportError(file, e);
}
return null; // does not happen
}
public static void importIntoTestRunSession(File swapFile,
TestRunSession testRunSession) throws CoreException {
try {
SAXParserFactory parserFactory = SAXParserFactory.newInstance();
// parserFactory.setValidating(true); // TODO: add DTD and debug
// flag
SAXParser parser = parserFactory.newSAXParser();
TestRunHandler handler = new TestRunHandler(testRunSession);
parser.parse(swapFile, handler);
} catch (ParserConfigurationException e) {
throwImportError(swapFile, e);
} catch (SAXException e) {
throwImportError(swapFile, e);
} catch (IOException e) {
throwImportError(swapFile, e);
}
}
/**
* Exports the given test run session.
*
* @param testRunSession
* the test run session
* @param file
* the destination
* @throws CoreException
*/
public static void exportTestRunSession(TestRunSession testRunSession,
File file) throws CoreException {
try (FileOutputStream out = new FileOutputStream(file)) {
exportTestRunSession(testRunSession, out);
} catch (IOException | TransformerException e) {
throwExportError(file, e);
}
}
public static void exportTestRunSession(TestRunSession testRunSession,
OutputStream out)
throws TransformerFactoryConfigurationError, TransformerException {
Transformer transformer = TransformerFactory.newInstance()
.newTransformer();
InputSource inputSource = new InputSource();
SAXSource source = new SAXSource(
new TestRunSessionSerializer(testRunSession), inputSource);
StreamResult result = new StreamResult(out);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
/*
* Bug in Xalan: Only indents if proprietary property
* org.apache.xalan.templates.OutputProperties.S_KEY_INDENT_AMOUNT is
* set.
*
* Bug in Xalan as shipped with J2SE 5.0: Does not read the
* indent-amount property at all >:-(.
*/
try {
transformer.setOutputProperty(
"{http://xml.apache.org/xalan}indent-amount", "2"); //$NON-NLS-1$ //$NON-NLS-2$
} catch (IllegalArgumentException e) {
// no indentation today...
}
transformer.transform(source, result);
}
private static void throwExportError(File file, Exception e)
throws CoreException {
throw new CoreException(new org.eclipse.core.runtime.Status(
IStatus.ERROR, DLTKTestingPlugin.getPluginId(),
Messages.format(ModelMessages.JUnitModel_could_not_write,
file.getAbsolutePath()),
e));
}
private static void throwImportError(File file, Exception e)
throws CoreException {
throw new CoreException(new org.eclipse.core.runtime.Status(
IStatus.ERROR, DLTKTestingPlugin.getPluginId(),
Messages.format(ModelMessages.JUnitModel_could_not_read,
file.getAbsolutePath()),
e));
}
/**
* Removes the given {@link TestRunSession} and notifies all registered
* {@link ITestRunSessionListener}s.
* <p>
* <b>To be called in the UI thread only!</b>
* </p>
*
* @param testRunSession
* the session to remove
*/
@Override
public void removeTestRunSession(TestRunSession testRunSession) {
final boolean existed;
synchronized (fTestRunSessions) {
existed = fTestRunSessions.remove(testRunSession);
}
if (existed) {
notifyTestRunSessionRemoved(testRunSession);
}
testRunSession.removeSwapFile();
}
private void notifyTestRunSessionRemoved(TestRunSession testRunSession) {
testRunSession.stopTestRun();
ILaunch launch = testRunSession.getLaunch();
if (launch != null) {
ILaunchManager launchManager = DebugPlugin.getDefault()
.getLaunchManager();
launchManager.removeLaunch(launch);
}
for (ITestRunSessionListener listener : fTestRunSessionListeners) {
listener.sessionRemoved(testRunSession);
}
}
private void notifyTestRunSessionAdded(TestRunSession testRunSession) {
for (ITestRunSessionListener listener : fTestRunSessionListeners) {
listener.sessionAdded(testRunSession);
}
}
}