All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.gemini.blueprint.test.AbstractOnTheFlyBundleCreatorTests Maven / Gradle / Ivy

Go to download

Eclipse Gemini Blueprint testing framework. Provides JUnit based integration testing inside OSGi containers.

There is a newer version: 3.0.0.M01
Show newest version
/******************************************************************************
 * Copyright (c) 2006, 2010 VMware Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * and Apache License v2.0 which accompanies this distribution. 
 * The Eclipse Public License is available at 
 * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0
 * is available at http://www.opensource.org/licenses/apache2.0.php.
 * You may elect to redistribute this code under either of these licenses. 
 * 
 * Contributors:
 *   VMware Inc.
 *****************************************************************************/

package org.eclipse.gemini.blueprint.test;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import org.eclipse.gemini.blueprint.test.internal.util.DependencyVisitor;
import org.eclipse.gemini.blueprint.test.internal.util.jar.JarCreator;
import org.eclipse.gemini.blueprint.util.OsgiStringUtils;
import org.objectweb.asm.ClassReader;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Enhanced subclass of {@link AbstractDependencyManagerTests} that facilitates
 * OSGi testing by creating at runtime, on the fly, a jar using the indicated
 * manifest and resource patterns (by default all files found under the root
 * path).
 * 
 * 

The test class can automatically determine the imports required by the * test, create the OSGi bundle manifest and pack the test and its resources in * a jar that can be installed inside an OSGi platform. * *

Additionally, a valid OSGi manifest is automatically created for the * resulting test if the user does not provide one. The classes present in the * archive are analyzed and based on their byte-code, the required * Import-Package entries (for packages not found in the bundle) * are created. * * Please see the reference documentation for an in-depth explanation and usage * examples. * *

Note that in more complex scenarios, dedicated packaging tools (such as * ant scripts or maven2) should be used. * *

It is recommend to extend {@link AbstractConfigurableBundleCreatorTests} * rather then this class as the former offers sensible defaults. * * @author Costin Leau * */ public abstract class AbstractOnTheFlyBundleCreatorTests extends AbstractDependencyManagerTests { private static final String META_INF_JAR_LOCATION = "/META-INF/MANIFEST.MF"; JarCreator jarCreator; /** field used for caching jar content */ private Map jarEntries; /** discovered manifest */ private Manifest manifest; public AbstractOnTheFlyBundleCreatorTests() { initializeJarCreator(); } public AbstractOnTheFlyBundleCreatorTests(String testName) { super(testName); initializeJarCreator(); } private void initializeJarCreator() { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { jarCreator = new JarCreator(); return null; } }); } /** * Returns the root path used for locating the resources that will be packed * in the test bundle (the root path does not become part of the jar). * *

By default, the current threads context ClassLoader is used to locate * the root of the classpath. Because unit tests will either be run from Maven * or an IDE this will resolve a test classes directory of sorts. * *

For example when invoked from Maven "file:./target/test-classes" * will be resolved and used. * * @return root path given as a String */ protected String getRootPath() { return Thread.currentThread().getContextClassLoader().getResource(".").toString(); } /** * Returns the patterns used for identifying the resources added to the jar. * The patterns are added to the root path when performing the search. By * default, the pattern is **/*. * *

In large test environments, performance can be improved by limiting * the resource added to the bundle by selecting only certain packages or * classes. This results in a small test bundle which is faster to create, * deploy and install. * * @return the patterns identifying the resources added to the jar */ protected String[] getBundleContentPattern() { return new String[] { JarCreator.EVERYTHING_PATTERN }; } /** * Returns the location (in Spring resource style) of the manifest location * to be used. By default null is returned, indicating that * the manifest should be picked up from the bundle content (if it's * available) or be automatically created based on the test class imports. * * @return the manifest location * @see #getManifest() * @see #createDefaultManifest() */ protected String getManifestLocation() { return null; } /** * Returns the current test bundle manifest. The method tries to read the * manifest from the given location; in case the location is * null (default), it will search for * META-INF/MANIFEST.MF file in jar content (as specified * through the patterns) and, if it cannot find the file, * automatically create a Manifest object * containing default entries. * *

Subclasses can override this method to enhance the returned * Manifest. * * @return Manifest used for this test suite. * * @see #createDefaultManifest() */ protected Manifest getManifest() { // return cached manifest if (manifest != null) return manifest; String manifestLocation = getManifestLocation(); if (StringUtils.hasText(manifestLocation)) { logger.info("Using Manifest from specified location=[" + getManifestLocation() + "]"); DefaultResourceLoader loader = new DefaultResourceLoader(); manifest = createManifestFrom(loader.getResource(manifestLocation)); } else { // set root path jarCreator.setRootPath(getRootPath()); // add the content pattern jarCreator.setContentPattern(getBundleContentPattern()); // see if the manifest already exists in the classpath // to resolve the patterns jarEntries = jarCreator.resolveContent(); for (Object o : jarEntries.entrySet()) { Map.Entry entry = (Map.Entry) o; if (META_INF_JAR_LOCATION.equals(entry.getKey())) { logger.info("Using Manifest from the test bundle content=[/META-INF/MANIFEST.MF]"); manifest = createManifestFrom((Resource) entry.getValue()); } } // fallback to default manifest creation if (manifest == null) { logger.info("Automatically creating Manifest for the test bundle"); manifest = createDefaultManifest(); } } return manifest; } /** * Indicates if the automatic manifest creation should consider only the * test class (true) or all classes included in the test * bundle(false). The latter should be used when the test * bundle contains additional classes that help with the test case. * *

By default, this method returns true, meaning that * only the test class will be searched for dependencies. * * @return true if only the test hierarchy is searched for dependencies or * false if all classes discovered in the test archive need to be * parsed. */ protected boolean createManifestOnlyFromTestClass() { return true; } private Manifest createManifestFrom(Resource resource) { Assert.notNull(resource, "unable to create manifest for empty resources"); try { return new Manifest(resource.getInputStream()); } catch (IOException ex) { throw (RuntimeException) new IllegalArgumentException("cannot create manifest from " + resource).initCause(ex); } } /** * Creates the default manifest in case none if found on the disk. By * default, the imports are synthetised based on the test class bytecode. * * @return default manifest for the jar created on the fly */ protected Manifest createDefaultManifest() { Manifest manifest = new Manifest(); Attributes attrs = manifest.getMainAttributes(); // manifest versions attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); attrs.putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); String description = getName() + "-" + getClass().getName(); // name/description attrs.putValue(Constants.BUNDLE_NAME, "TestBundle-" + description); attrs.putValue(Constants.BUNDLE_SYMBOLICNAME, "TestBundle-" + description); attrs.putValue(Constants.BUNDLE_DESCRIPTION, "on-the-fly test bundle"); // activator attrs.putValue(Constants.BUNDLE_ACTIVATOR, JUnitTestActivator.class.getName()); // add Import-Package entry addImportPackage(manifest); if (logger.isDebugEnabled()) { logger.debug("Created manifest:" + manifest.getMainAttributes().entrySet()); } return manifest; } private void addImportPackage(Manifest manifest) { String[] rawImports = determineImports(); boolean trace = logger.isTraceEnabled(); if (trace) { logger.trace("Discovered raw imports " + ObjectUtils.nullSafeToString(rawImports)); } Collection specialImportsOut = eliminateSpecialPackages(rawImports); Collection imports = eliminatePackagesAvailableInTheJar(specialImportsOut); if (trace) { logger.trace("Filtered imports are " + imports); } manifest.getMainAttributes().putValue(Constants.IMPORT_PACKAGE, StringUtils.collectionToCommaDelimitedString(imports)); } /** * Eliminate 'special' packages (java.*, test framework internal and the * class declaring package) * * @param rawImports raw imports * @return cleaned up imports */ private Collection eliminateSpecialPackages(String[] rawImports) { String currentPckg = ClassUtils.classPackageAsResourcePath(getClass()).replace('/', '.'); Set filteredImports = new LinkedHashSet(rawImports.length); Set eliminatedImports = new LinkedHashSet(4); for (int i = 0; i < rawImports.length; i++) { String pckg = rawImports[i]; if (!(pckg.startsWith("java.") || pckg.startsWith("org.eclipse.gemini.blueprint.test.internal") || pckg.equals(currentPckg))) { filteredImports.add(pckg); } else { eliminatedImports.add(pckg); } } if (!eliminatedImports.isEmpty() && logger.isTraceEnabled()) { logger.trace("Eliminated special packages " + eliminatedImports); } return filteredImports; } /** * Eliminates imports for packages already included in the bundle. Works * only if the jar content is known (variable 'jarEntries' set). * * @param imports * @return */ private Collection eliminatePackagesAvailableInTheJar(Collection imports) { // no jar entry present, bail out. if (jarEntries == null || jarEntries.isEmpty()) { return imports; } Set filteredImports = new LinkedHashSet(imports.size()); Collection eliminatedImports = new LinkedHashSet(2); Collection jarPackages = jarCreator.getContainedPackages(); for (Iterator iterator = imports.iterator(); iterator.hasNext();) { String pckg = (String) iterator.next(); if (jarPackages.contains(pckg)) { eliminatedImports.add(pckg); } else { filteredImports.add(pckg); } } if (!eliminatedImports.isEmpty() && logger.isTraceEnabled()) logger.trace("Eliminated packages already present in the bundle " + eliminatedImports); return filteredImports; } /** * Determine imports for the given bundle. Based on the user settings, this * method will consider only the the test hierarchy until the testing * framework is found or all classes available inside the test bundle.

* Note that split packages are not supported. * * @return */ private String[] determineImports() { boolean useTestClassOnly = false; // no jar entry present, bail out. if (jarEntries == null || jarEntries.isEmpty()) { logger.debug("No test jar content detected, generating bundle imports from the test class"); useTestClassOnly = true; } else if (createManifestOnlyFromTestClass()) { logger.info("Using the test class for generating bundle imports"); useTestClassOnly = true; } else { logger.info("Using all classes in the jar for the generation of bundle imports"); } // className, class resource Map entries; if (useTestClassOnly) { entries = new LinkedHashMap(4); // get current class (test class that bootstraps the OSGi infrastructure) Class clazz = getClass(); String clazzPackage = null; String endPackage = AbstractOnTheFlyBundleCreatorTests.class.getPackage().getName(); do { // consider inner classes as well List classes = new ArrayList(4); classes.add(clazz); CollectionUtils.mergeArrayIntoCollection(clazz.getDeclaredClasses(), classes); for (Iterator iterator = classes.iterator(); iterator.hasNext();) { Class classToInspect = (Class) iterator.next(); Package pkg = classToInspect.getPackage(); if (pkg != null) { clazzPackage = pkg.getName(); String classFile = ClassUtils.getClassFileName(classToInspect); entries.put(classToInspect.getName().replace('.', '/').concat(ClassUtils.CLASS_FILE_SUFFIX), new InputStreamResource(classToInspect.getResourceAsStream(classFile))); } // handle default package else { logger.warn("Could not find package for class " + classToInspect + "; ignoring..."); } } clazz = clazz.getSuperclass(); } while (!endPackage.equals(clazzPackage)); } else { entries = jarEntries; } return determineImportsFor(entries); } private String[] determineImportsFor(Map entries) { // get contained packages to do matching on the test hierarchy Collection containedPackages = jarCreator.getContainedPackages(); Set cumulatedPackages = new LinkedHashSet(); // make sure the collection package is valid boolean validPackageCollection = !containedPackages.isEmpty(); boolean trace = logger.isTraceEnabled(); for (Iterator iterator = entries.entrySet().iterator(); iterator.hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String resourceName = (String) entry.getKey(); // filter out the test hierarchy if (resourceName.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) { if (trace) { logger.trace("Analyze imports for test bundle resource " + resourceName); } String classFileName = StringUtils.getFilename(resourceName); String className = classFileName.substring(0, classFileName.length() - ClassUtils.CLASS_FILE_SUFFIX.length()); String classPkg = resourceName.substring(0, resourceName.length() - classFileName.length()).replace('/', '.'); if (classPkg.startsWith(".")) { classPkg = classPkg.substring(1); } if (classPkg.endsWith(".")) { classPkg = classPkg.substring(0, classPkg.length() - 1); } // if we don't have the package, add it if (validPackageCollection && StringUtils.hasText(classPkg) && !containedPackages.contains(classPkg)) { if (trace) { logger.trace("Package [" + classPkg + "] is NOT part of the test archive; adding an import for it"); } cumulatedPackages.add(classPkg); } // otherwise parse the class byte-code else { if (trace) { logger.trace("Package [" + classPkg + "] is part of the test archive; parsing " + className + " bytecode to determine imports..."); } cumulatedPackages.addAll(determineImportsForClass(className, (Resource) entry.getValue())); } } } return (String[]) cumulatedPackages.toArray(new String[cumulatedPackages.size()]); } /** * Determine imports for a class given as a String resource. This method * doesn't do any search for the enclosing/inner classes as it considers * that these should be handled at a higher level. * * The returned set contains the packages in string format (i.e. java.io) * * @param className * @param resource * @return */ private Set determineImportsForClass(String className, Resource resource) { Assert.notNull(resource, "a not-null class is required"); DependencyVisitor visitor = new DependencyVisitor(); boolean trace = logger.isTraceEnabled(); ClassReader reader; try { if (trace) { logger.trace("Visiting class " + className); } reader = new ClassReader(resource.getInputStream()); } catch (Exception ex) { throw (RuntimeException) new IllegalArgumentException("Cannot read class " + className).initCause(ex); } reader.accept(visitor, false); // convert from / to . format Set originalPackages = visitor.getPackages(); Set pkgs = new LinkedHashSet(originalPackages.size()); for (Iterator iterator = originalPackages.iterator(); iterator.hasNext();) { String pkg = (String) iterator.next(); pkgs.add(pkg.replace('/', '.')); } return pkgs; } protected void postProcessBundleContext(BundleContext context) throws Exception { logger.debug("Post processing: creating test bundle"); Resource jar; Manifest mf = getManifest(); // if the jar content hasn't been discovered yet (while creating the manifest) // do so now if (jarEntries == null) { // set root path jarCreator.setRootPath(getRootPath()); // add the content pattern jarCreator.setContentPattern(getBundleContentPattern()); // use jar creator for pattern discovery jar = jarCreator.createJar(mf); } // otherwise use the cached resources else { jar = jarCreator.createJar(mf, jarEntries); } try { installAndStartBundle(context, jar); } catch (Exception e) { IllegalStateException ise = new IllegalStateException( "Unable to dynamically start generated unit test bundle"); ise.initCause(e); throw ise; } // now do the delegation super.postProcessBundleContext(context); } private void installAndStartBundle(BundleContext context, Resource resource) throws Exception { // install & start Bundle bundle = context.installBundle("[onTheFly-test-bundle]" + ClassUtils.getShortName(getClass()) + "[" + hashCode() + "]", resource.getInputStream()); String bundleString = OsgiStringUtils.nullSafeNameAndSymName(bundle); boolean debug = logger.isDebugEnabled(); if (debug) { logger.debug("Test bundle [" + bundleString + "] successfully installed"); logger.debug(Constants.FRAMEWORK_BOOTDELEGATION + " = " + context.getProperty(Constants.FRAMEWORK_BOOTDELEGATION)); } bundle.start(); if (debug) { logger.debug("Test bundle [" + bundleString + "] successfully started"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy