/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.core.test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.eclipse.core.internal.registry.ExtensionRegistry;
import org.eclipse.core.internal.registry.osgi.ExtensionEventDispatcherJob;
import org.eclipse.core.runtime.ContributorFactoryOSGi;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IExtensionPoint;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.RegistryFactory;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.riena.core.exception.MurphysLawFailure;
import org.eclipse.riena.core.util.IOUtils;
import org.eclipse.riena.core.util.Nop;
import org.eclipse.riena.core.util.Trace;
import org.eclipse.riena.internal.core.ignore.IgnoreFindBugs;
/**
* Base class for test cases.<br>
* It extends the {@link junit.framework.TestCase} with a few helpers.
*
* @since 5.0
*/
// this is for org.eclipse.core.internal.registry.ExtensionRegistry
@SuppressWarnings("restriction")
public class TestingTools {
public interface IClosure {
void execute(Bundle bundle) throws BundleException;
}
interface TestCaseWrapper {
/**
* @return
*/
String getName();
/**
* @param string
*/
void fail(String string);
/**
* @param success
*/
void assertTrue(boolean success);
/**
* @param o
*/
void assertNotNull(Object o);
/**
* @return
*/
Class<?> getTestClass();
}
// Keep track of services and corresponding service references.
private final Map<Object, ServiceReference> services = new HashMap<Object, ServiceReference>();
// Do not access this field directly! Use the getter getContext() because this does a lazy initialization.
private BundleContext context;
private final ExtensionRegistryChangeJobTracker jobTracker = new ExtensionRegistryChangeJobTracker();
private final TestCaseWrapper testCase;
private final boolean trace;
/**
* @param testClass
*
*/
TestingTools(final TestCaseWrapper testCase) {
this.testCase = testCase;
trace = Trace.isOn(RienaTestCase.class, testCase.getTestClass(), "debug"); //$NON-NLS-1$
}
/**
* A counterpart to Assert.fail() that may be invoked to indicate that everything is fine and that the test should continue. May be used e.g. in an
* otherwise empty catch block that handles an expected exception. In this use case its advantages over a comment are that it allows a more uniform way of
* documentation than the numerous variations of "// ignore" and that it avoids a Checkstyle warning about the empty block.
*/
protected void ok() {
// nothing to do, everything is OK...
}
/**
* A counterpart to Assert.fail(String) that may be invoked to indicate that everything is fine and that the test should continue.
*
* @see #ok()
*
* @param message
* A message explaining why nothing is wrong.
*/
public void ok(final String message) {
ok();
}
void setUp() {
services.clear();
}
void tearDown() {
for (final ServiceReference reference : services.values()) {
getContext().ungetService(reference);
}
services.clear();
}
/**
* Return the bundle context. <br>
* <b>Note: </b>This method must not be called from a constructor of a test case!
*
* @return
*/
public BundleContext getContext() {
if (context == null) {
try {
final Bundle bundle = FrameworkUtil.getBundle(testCase.getTestClass());
context = bundle.getBundleContext();
} catch (final Throwable t) {
Nop.reason("We don�t care. Maybe it is not running as a plugin test."); //$NON-NLS-1$
}
}
return context;
}
/**
* Get the resource located in the folder along with the test case.
* <p>
* <b>Note:</b> The resource will be copied into a temporary file and this file will be returned. This file will be deleted on JVM exit.
*
* @param resource
* @return a file to the content of the resource
*/
@IgnoreFindBugs(value = { "UI_INHERITANCE_UNSAFE_GETRESOURCE" }, justification = "testCase.getTestClass().getResource() shall return the resource relative to the sub-class, because this is the unit test which needs the 'local' resource.")
public File getFile(final String resource) {
final URL url = testCase.getTestClass().getResource(resource);
try {
final File tempFile = File.createTempFile(resource.replace('.', '-'), ".tmp"); //$NON-NLS-1$
tempFile.deleteOnExit();
IOUtils.copy(url.openStream(), new FileOutputStream(tempFile));
return tempFile;
} catch (final IOException e) {
testCase.fail("IOException when trying to make a copy of " + resource + ": " + e); //$NON-NLS-1$ //$NON-NLS-2$
return null; // make compiler happy
}
}
/**
* Check whether trace is switched on or not.
*
* @return tracing?
*/
public boolean isTrace() {
return trace;
}
/**
* Print the current test�s name.
*/
public void printTestName() {
if (!isTrace()) {
return;
}
System.out.println(testCase.getName());
for (int i = 0; i < testCase.getName().length(); i++) {
System.out.print('-');
}
System.out.println();
}
/**
* Print the string, no CR/LF.
*
* @param string
*/
public void print(final String string) {
if (!isTrace()) {
return;
}
System.out.print(string);
}
/**
* Print the string, with CR/LF.
*
* @param string
*/
public void println(final String string) {
if (!isTrace()) {
return;
}
System.out.println(string);
}
/**
* Add an extension/extension point defined within the �plugin.xml� (located along side the test class) given with the <code>pluginResource</code> to the
* extension registry.
*
* @param pluginResource
* @throws InterruptedException
*/
public void addPluginXml(final String pluginResource) {
addPluginXml(testCase.getTestClass(), pluginResource);
}
/**
* Add an extension/extension point defined within the �plugin.xml� given with the <code>pluginResource</code> to the extension registry.
*
* @param forLoad
* @param pluginResource
* @throws InterruptedException
*/
public void addPluginXml(final Class<?> forLoad, final String pluginResource) {
try {
final IExtensionRegistry registry = RegistryFactory.getRegistry();
@IgnoreFindBugs(value = "OBL_UNSATISFIED_OBLIGATION", justification = "stream will be closed by getResourceAsStream()")
final InputStream inputStream = forLoad.getResourceAsStream(pluginResource);
final IContributor contributor = ContributorFactoryOSGi.createContributor(getContext().getBundle());
startJobTracking();
final boolean success = registry.addContribution(inputStream, contributor, false, pluginResource, null,
((ExtensionRegistry) registry).getTemporaryUserToken());
stopJobTracking();
testCase.assertTrue(success);
joinTrackedJobs();
} catch (final Exception e) {
}
}
private void startJobTracking() {
jobTracker.start();
}
private void stopJobTracking() {
jobTracker.stop();
}
private void joinTrackedJobs() {
jobTracker.joinJobs();
}
private class ExtensionRegistryChangeJobTracker extends JobChangeAdapter {
private List<Job> jobs;
protected void start() {
jobs = new ArrayList<Job>();
Job.getJobManager().addJobChangeListener(this);
}
protected void stop() {
Job.getJobManager().removeJobChangeListener(this);
}
protected void joinJobs() {
for (final Job job : jobs) {
try {
job.join();
} catch (final InterruptedException e) {
throw new MurphysLawFailure("Joining jobs failed", e); //$NON-NLS-1$
}
}
}
@Override
public void scheduled(final IJobChangeEvent event) {
if (event.getJob() instanceof ExtensionEventDispatcherJob) {
jobs.add(event.getJob());
}
}
}
/**
* Remove the given extension from the extension registry.
*
* @param extensionId
*/
public void removeExtension(final String extensionId) {
final IExtensionRegistry registry = RegistryFactory.getRegistry();
final IExtension extension = registry.getExtension(extensionId);
testCase.assertNotNull(extension);
startJobTracking();
final boolean success = registry.removeExtension(extension, ((ExtensionRegistry) registry).getTemporaryUserToken());
stopJobTracking();
testCase.assertTrue(success);
joinTrackedJobs();
}
/**
* Remove the given extension from the extension registry.
*
* @param extensionPointId
*/
public void removeExtensionPoint(final String extensionPointId) {
final IExtensionRegistry registry = RegistryFactory.getRegistry();
final IExtensionPoint extensionPoint = registry.getExtensionPoint(extensionPointId);
testCase.assertNotNull(extensionPoint);
startJobTracking();
final boolean success = registry.removeExtensionPoint(extensionPoint, ((ExtensionRegistry) registry).getTemporaryUserToken());
stopJobTracking();
testCase.assertTrue(success);
joinTrackedJobs();
}
/**
* Get the service for the specified <code>serviceClass</code>.
*
* @param serviceClass
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getService(final Class<T> serviceClass) {
final ServiceReference reference = getContext().getServiceReference(serviceClass.getName());
if (reference == null) {
return null;
}
final Object service = getContext().getService(reference);
if (service == null) {
return null;
}
services.put(service, reference);
return (T) service;
}
/**
* Unget the specified <code>service</code>.
*
* @param service
*/
public void ungetService(final Object service) {
final ServiceReference reference = services.get(service);
if (reference == null) {
return;
}
getContext().ungetService(reference);
}
/**
* Starts the bundle with the given <code>bundleName</code>.
*
* @param bundleName
* @throws BundleException
*/
public void startBundle(final String bundleName) throws BundleException {
startBundles(bundleName.replaceAll("\\.", "\\\\."), null); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Starts all bundles that match the <code>includePattern</code> but not the <code>excludePattern</code>. The <code>excludePattern</code> may be
* <code>null</code>.
*
* @param includePattern
* @param excludePattern
* @throws BundleException
*/
public void startBundles(final String includePattern, final String excludePattern) throws BundleException {
doWithBundles(includePattern, excludePattern, new IClosure() {
public void execute(final Bundle bundle) throws BundleException {
if (bundle.getState() == Bundle.RESOLVED || bundle.getState() == Bundle.STARTING /*
* STARTING == LAZY
*/) {
bundle.start();
} else {
if (bundle.getState() == Bundle.INSTALLED) {
throw new RuntimeException("can't start required bundle because it is not RESOLVED but only INSTALLED : " //$NON-NLS-1$
+ bundle.getSymbolicName());
}
}
}
});
}
/**
* Stops the bundle with the given <code>bundleName</code>.
*
* @param bundleName
* @throws BundleException
*/
public void stopBundle(final String bundleName) throws BundleException {
stopBundles(bundleName.replaceAll("\\.", "\\\\."), null); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Stops all bundles that match the <code>includePattern</code> but not the <code>excludePattern</code>. The <code>excludePattern</code> may be
* <code>null</code>.
*
* @param includePattern
* @param excludePattern
* @throws BundleException
*/
public void stopBundles(final String includePattern, final String excludePattern) throws BundleException {
doWithBundles(includePattern, excludePattern, new IClosure() {
public void execute(final Bundle bundle) throws BundleException {
if (bundle.getState() == Bundle.ACTIVE) {
bundle.stop();
} else {
if (bundle.getState() != Bundle.UNINSTALLED) {
Nop.reason("testcase tried to stop this bundle which did not run, but we can ignore this ==> bundle is stopped already"); //$NON-NLS-1$
}
}
}
});
}
/**
* IClosure with all bundles that match the <code>includePattern</code> but not the <code>excludePattern</code> what is specified within the
* <code>closure</code>. The <code>excludePattern</code> may be <code>null</code>.
*
* @param includePattern
* @param excludePattern
* @param closure
* @throws BundleException
*/
public void doWithBundles(final String includePattern, String excludePattern, final IClosure closure) throws BundleException {
if (includePattern == null) {
throw new UnsupportedOperationException("truePattern must be set"); //$NON-NLS-1$
}
if (excludePattern == null) {
excludePattern = ""; //$NON-NLS-1$
}
final Pattern include = Pattern.compile(includePattern);
final Pattern exclude = Pattern.compile(excludePattern);
final Bundle[] bundles = getContext().getBundles();
for (final Bundle bundle : bundles) {
if (include.matcher(bundle.getSymbolicName()).matches() && !(exclude.matcher(bundle.getSymbolicName()).matches())) {
closure.execute(bundle);
}
}
}
}