/* * Copyright 2017 ThoughtWorks, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.thoughtworks.go.plugin.activation; import com.googlecode.junit.ext.checkers.OSChecker; import com.thoughtworks.go.plugin.activation.test.*; import com.thoughtworks.go.plugin.api.TestGoPluginExtensionPoint; import com.thoughtworks.go.plugin.api.annotation.Extension; import com.thoughtworks.go.plugin.api.info.PluginDescriptor; import com.thoughtworks.go.plugin.api.info.PluginDescriptorAware; import com.thoughtworks.go.plugin.infra.FelixGoPluginOSGiFramework; import com.thoughtworks.go.plugin.infra.plugininfo.DefaultPluginRegistry; import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginDescriptor; import com.thoughtworks.go.util.ReflectionUtil; import com.thoughtworks.go.util.SystemEnvironment; import com.thoughtworks.go.util.ZipUtil; import lib.test.DummyPluginAwareExtensionInLibDirectory; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.hamcrest.core.Is; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.ops4j.pax.tinybundles.core.InnerClassStrategy; import org.ops4j.pax.tinybundles.core.TinyBundle; import org.ops4j.pax.tinybundles.core.TinyBundles; import org.osgi.framework.*; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.Arrays; import java.util.zip.ZipInputStream; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.*; public class DefaultGoPluginActivatorIntegrationTest { private static final File TMP_DIR = new File("./tmp"); private static final String BUNDLE_DIR_WHICH_HAS_PROPER_ACTIVATOR = "DefaultGoPluginActivatorIntegrationTest.bundleDirWhichHasProperActivator"; public static final String NO_EXT_ERR_MSG = "No extensions found in this plugin.Please check for @Extension annotations"; public static final String GO_TEST_DUMMY_SYMBOLIC_NAME = "Go-Test-Dummy-Symbolic-Name"; private FelixGoPluginOSGiFramework framework; private StubOfDefaultPluginRegistry registry; @Before public void setUp() throws Exception { registry = new StubOfDefaultPluginRegistry(); framework = new FelixGoPluginOSGiFramework(registry, new SystemEnvironment()); framework.start(); } @Test public void shouldRegisterAClassImplementingPluginDescriptorAwareAsAnOSGiService() throws Exception { assertThatPluginWithThisExtensionClassLoadsSuccessfully(DummyPluginAwareExtension.class); } @Test public void shouldNotRegisterAsAnOSGiServiceAClassImplementingPluginDescriptorAwareWithoutAPublicConstructor() throws Exception { Bundle bundle = installBundleWithClasses(DummyPluginAwareExtensionWithNonPublicDefaultConstructor.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterAsAnOSGiServiceAClassImplementingPluginDescriptorAwareWithOnlyAOneArgConstructor() throws Exception { Bundle bundle = installBundleWithClasses(DummyPluginAwareExtensionWithOneArgConstructorOnly.class); assertThat(bundle.getState(),is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); String error = descriptor.getStatus().getMessages().get(0); assertThat(error.contains("DummyPluginAwareExtensionWithOneArgConstructorOnly"),is(true)); assertThat(error.contains("Make sure it and all of its parent classes have a default constructor."),is(true)); } @Test public void shouldNotRegisterAsAnOSGiServiceAnExtensionClassWhichDoesNotImplementAGoExtensionPoint() throws Exception { Bundle bundle = installBundleWithClasses(NotAGoExtensionPoint.class, NotAGoExtensionAsItDoesNotImplementAnyExtensionPoints.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotLoadClassesFoundInMETA_INFEvenIfTheyAreProperGoExtensionPoints() throws Exception { File bundleWithActivator = createBundleWithActivator(BUNDLE_DIR_WHICH_HAS_PROPER_ACTIVATOR, DummyPluginAwareExtension.class); File sourceClassFile = new File(bundleWithActivator, "com/thoughtworks/go/plugin/activation/test/DummyPluginAwareExtension.class"); File destinationFile = new File(bundleWithActivator, "META-INF/com/thoughtworks/go/plugin/activation/test/"); FileUtils.moveFileToDirectory(sourceClassFile, destinationFile, true); Bundle bundle = installBundleFoundInDirectory(bundleWithActivator); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG),is(true)); } @Test public void shouldNotFailToRegisterOtherClassesIfAClassCannotBeLoadedBecauseOfWrongPath() throws Exception { File bundleWithActivator = createBundleWithActivator(BUNDLE_DIR_WHICH_HAS_PROPER_ACTIVATOR, DummyPluginAwareExtension.class); File sourceClassFile = new File(bundleWithActivator, "com/thoughtworks/go/plugin/activation/test/DummyPluginAwareExtension.class"); File destinationFile = new File(bundleWithActivator, "ABC-DEF/com/thoughtworks/go/plugin/activation/test/"); FileUtils.copyFileToDirectory(sourceClassFile, destinationFile, true); Bundle bundle = installBundleFoundInDirectory(bundleWithActivator); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); } @Test public void shouldNotLoadAClassFoundInLibDirectoryEvenIfItIsAProperGoExtensionPoints() throws Exception { Bundle bundle = installBundleWithClasses(DummyPluginAwareExtensionInLibDirectory.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterAsAnOSGiServiceAClassWhichIsAbstract() throws Exception { Bundle bundle = installBundleWithClasses(AbstractPluginAwareExtension.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterAsAnOSGiServiceAClassWhichIsNotPublic() throws Exception { Bundle bundle = installBundleWithClasses(DummyPluginAwareExtensionWhichIsNotPublic.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterAsAnOSGiServiceAnInterfaceEvenIfItImplementsAGoExtensionPointInterface() throws Exception { Bundle bundle = installBundleWithClasses(PluginAwareExtensionInterface.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterAsAnOSGiServiceAClassWhichThrowsExceptionDuringInstantiation() throws Exception { Bundle bundle = installBundleWithClasses(DummyPluginAwareExtension.class, DummyPluginAwareExtensionWhichThrowsAnExceptionDuringConstruction.class); assertThat(bundle.getState(),is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); String error = descriptor.getStatus().getMessages().get(0); assertThat(error.contains("DummyPluginAwareExtensionWhichThrowsAnExceptionDuringConstruction"), is(true)); assertThat(error.contains("java.lang.RuntimeException: Ouch! I failed!"), is(true)); } @Test public void shouldRegisterANestedClassImplementingPluginDescriptorAwareAsAnOSGiService() throws Exception { if (new OSChecker(OSChecker.WINDOWS).satisfy()) { return; // The class files in this test become too big for a Windows filesystem to handle. } File bundleWithActivator = createBundleWithActivator(BUNDLE_DIR_WHICH_HAS_PROPER_ACTIVATOR, PluginAwareExtensionOuterClass.class, PluginAwareExtensionOuterClass.NestedClass.class, PluginAwareExtensionOuterClass.InnerClass.class, PluginAwareExtensionOuterClass.InnerClass.SecondLevelInnerClass.class, PluginAwareExtensionOuterClass.InnerClass.SecondLevelInnerClass.PluginAwareExtensionThirdLevelInnerClass.class, PluginAwareExtensionOuterClass.InnerClass.SecondLevelSiblingInnerClassNoDefaultConstructor.class); BundleContext installedBundledContext = bundleContext(installBundleFoundInDirectory(bundleWithActivator)); ServiceReference<?>[] references = installedBundledContext.getServiceReferences(PluginDescriptorAware.class.getName(), null); String[] services = toSortedServiceClassNames(installedBundledContext, references); assertEquals(Arrays.toString(services), 4, services.length); assertEquals(PluginAwareExtensionOuterClass.class.getName(), services[0]); assertEquals(PluginAwareExtensionOuterClass.InnerClass.class.getName(), services[1]); assertEquals(PluginAwareExtensionOuterClass.InnerClass.SecondLevelInnerClass.PluginAwareExtensionThirdLevelInnerClass.class.getName(), services[2]); assertEquals(PluginAwareExtensionOuterClass.NestedClass.class.getName(), services[3]); } @Test public void shouldRegisterAsAnOSGiServiceADerivedClassWhoseAncestorImplementsAnExtensionPoint() throws Exception { BundleContext installedBundledContext = bundleContext(installBundleWithClasses(PluginAwareExtensionThatIsADerivedClass.class, DummyPluginAwareExtension.class, PluginAwareExtensionThatIsADerivedClass.class.getSuperclass())); ServiceReference<?>[] references = installedBundledContext.getServiceReferences(PluginDescriptorAware.class.getName(), null); String[] services = toSortedServiceClassNames(installedBundledContext, references); assertEquals(Arrays.toString(services), 2, services.length); assertEquals(DummyPluginAwareExtension.class.getName(), services[0]); assertEquals(PluginAwareExtensionThatIsADerivedClass.class.getName(), services[1]); } @Test public void shouldRegisterOneInstanceForEachExtensionPointAnExtensionImplements() throws Exception { BundleContext installedBundledContext = bundleContext(installBundleWithClasses(TestGoPluginExtensionThatImplementsTwoExtensionPoints.class, DummyPluginAwareExtension.class)); ServiceReference<?>[] references = installedBundledContext.getServiceReferences(PluginDescriptorAware.class.getName(), null); String[] services = toSortedServiceClassNames(installedBundledContext, references); assertEquals(Arrays.toString(services), 2, services.length); assertEquals(DummyPluginAwareExtension.class.getName(), services[0]); assertEquals(TestGoPluginExtensionThatImplementsTwoExtensionPoints.class.getName(), services[1]); references = installedBundledContext.getServiceReferences(TestGoPluginExtensionPoint.class.getName(), null); assertEquals(1, references.length); assertEquals(TestGoPluginExtensionThatImplementsTwoExtensionPoints.class.getName(), installedBundledContext.getService(references[0]).getClass().getName()); Object testExtensionImplementation = getImplementationOfType(installedBundledContext, references, TestGoPluginExtensionThatImplementsTwoExtensionPoints.class); references = installedBundledContext.getServiceReferences(PluginDescriptorAware.class.getName(), null); assertEquals(2, references.length); Object descriptorAwareImplementation = getImplementationOfType(installedBundledContext, references, TestGoPluginExtensionThatImplementsTwoExtensionPoints.class); assertSame(testExtensionImplementation, descriptorAwareImplementation); } @Test public void shouldRegisterOneInstanceForEachExtensionPointWhereThePluginClassExtendsABaseClassWhichIsAnExtensionAndImplementsAGoExtensionPoint() throws Exception { BundleContext installedBundledContext = bundleContext(installBundleWithClasses(ClassThatExtendsTestExtensionPoint.class, ClassThatExtendsTestExtensionPoint.ClassThatExtendsTwoGoExtensionPoint.class, TestGoPluginExtensionPoint.class)); ServiceReference<?>[] references = installedBundledContext.getServiceReferences(TestGoPluginExtensionPoint.class.getName(), null); assertEquals(1, references.length); Object testExtensionImplementation = getImplementationOfType(installedBundledContext, references, ClassThatExtendsTestExtensionPoint.ClassThatExtendsTwoGoExtensionPoint.class); references = installedBundledContext.getServiceReferences(PluginDescriptorAware.class.getName(), null); assertEquals(1, references.length); Object descriptorAwareImplementation = getImplementationOfType(installedBundledContext, references, ClassThatExtendsTestExtensionPoint.ClassThatExtendsTwoGoExtensionPoint.class); assertSame(testExtensionImplementation, descriptorAwareImplementation); } @Test public void shouldNotRegisterAnAnonymousClassThatImplementsAnExtensionPoint() throws BundleException, IOException, URISyntaxException, InvalidSyntaxException { Bundle bundle = installBundleWithClasses(DummyClassProvidingAnonymousClass.getAnonymousClass().getClass()); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG),is(true)); } @Test public void shouldNotRegisterAnAnonymousClassDefinedWithinAnInnerClassThatImplementsAnExtensionPoint() throws BundleException, IOException, URISyntaxException, InvalidSyntaxException { Bundle bundle = installBundleWithClasses(DummyClassProvidingAnonymousClass.DummyInnerClassProvidingAnonymousClass.getAnonymousClass().getClass()); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterLocalInnerClassesThatImplementAnExtensionPoint() throws BundleException, IOException, URISyntaxException, InvalidSyntaxException { Bundle bundle = installBundleWithClasses(DummyClassWithLocalInnerClass.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldNotRegisterPublicInnerClassesThatImplementAnExtensionPointInsidePackageLevelClass() throws BundleException, IOException, URISyntaxException, InvalidSyntaxException { Bundle bundle = installBundleWithClasses(PackageLevelClassWithPublicInnerClass.class, PackageLevelClassWithPublicInnerClass.DummyInnerClassWithExtension.class); assertThat(bundle.getState(), is(Bundle.UNINSTALLED)); GoPluginDescriptor descriptor = registry.getPlugin(GO_TEST_DUMMY_SYMBOLIC_NAME); assertThat(descriptor.isInvalid(), is(true)); assertThat(descriptor.getStatus().getMessages().contains(NO_EXT_ERR_MSG), is(true)); } @Test public void shouldBeAbleToUsePackagesFromJavaxWithinThePluginSinceItHasBeenExportedUsingBootDelegationInTheOSGIFramework() throws Exception { assertThatPluginWithThisExtensionClassLoadsSuccessfully(ClassWhichUsesSomeClassInJavaxPackage.class); } @Test public void shouldBeAbleToUsePackagesFromOrgXmlSaxPackageWithinThePluginSinceItHasBeenExportedUsingBootDelegationInTheOSGIFramework() throws Exception { assertThatPluginWithThisExtensionClassLoadsSuccessfully(ClassWhichUsesSomeClassesInOrgXMLSaxPackage.class); } @Test public void shouldBeAbleToUsePackagesFromOrgW3cDomPackageWithinThePluginSinceItHasBeenExportedUsingBootDelegationInTheOSGIFramework() throws Exception { assertThatPluginWithThisExtensionClassLoadsSuccessfully(ClassWhichUsesSomeClassesInOrgW3CDomPackage.class); } @After public void tearDown() throws Exception { framework.stop(); FileUtils.deleteDirectory(TMP_DIR); } private void assertThatPluginWithThisExtensionClassLoadsSuccessfully(Class<?> extensionClass) throws IOException, URISyntaxException, BundleException, InvalidSyntaxException { BundleContext installedBundleContext = bundleContext(installBundleWithClasses(extensionClass)); ServiceReference<?>[] references = installedBundleContext.getServiceReferences(PluginDescriptorAware.class.getName(), null); assertEquals("No service registered for PluginDescriptorAware class", 1, references.length); assertEquals("Symbolic Name property should be present", GO_TEST_DUMMY_SYMBOLIC_NAME, references[0].getProperty(Constants.BUNDLE_SYMBOLICNAME)); assertEquals(extensionClass.getName(), installedBundleContext.getService(references[0]).getClass().getName()); } private void assertNoReferencesFor(Class<?> clazz, BundleContext installedBundledContext) throws InvalidSyntaxException { ServiceReference<?>[] references = installedBundledContext.getServiceReferences(clazz.getName(), null); assertNull("Found references: " + Arrays.toString(references), references); } private void verifyActivatorHasErrors(Bundle bundle) { Object m_activator = ReflectionUtil.getField(bundle, "m_activator"); try { Object hasErrors = ReflectionUtil.invoke(m_activator, "hasErrors"); assertThat(hasErrors, Is.is(true)); } catch (Exception e) { e.printStackTrace(); } } private String[] toSortedServiceClassNames(BundleContext installedBundledContext, ServiceReference<?>[] references) { if (references == null) { return new String[0]; } String[] services = new String[references.length]; for (int i = 0; i < references.length; i++) { ServiceReference<?> reference = references[i]; services[i] = installedBundledContext.getService(reference).getClass().getName(); } Arrays.sort(services); return services; } private Object getImplementationOfType(BundleContext installedBundledContext, ServiceReference<?>[] references, Class<?> type) { if (references == null) { return new String[0]; } for (ServiceReference<?> reference : references) { Object service = installedBundledContext.getService(reference); if (service.getClass().getName().equals(type.getName())) { return service; } } throw new RuntimeException("Class type not found: " + type); } private Bundle installBundleWithClasses(Class... classesToBeLoaded) throws IOException, URISyntaxException, BundleException { return installBundleFoundInDirectory(createBundleWithActivator(BUNDLE_DIR_WHICH_HAS_PROPER_ACTIVATOR, classesToBeLoaded)); } private Bundle installBundleFoundInDirectory(File bundleWithActivator) throws BundleException { GoPluginDescriptor pluginDescriptor = new GoPluginDescriptor(GO_TEST_DUMMY_SYMBOLIC_NAME, "1", null, null, bundleWithActivator, true); registry.fakeRegistrationOfPlugin(pluginDescriptor); return framework.loadPlugin(pluginDescriptor); } private BundleContext bundleContext(Bundle bundle){ return bundle.getBundleContext(); } private File createBundleWithActivator(String destinationDir, Class... classesToBeAdded) throws IOException, URISyntaxException { TinyBundle bundleBeingBuilt = TinyBundles.bundle() .add(GoPluginActivator.class) .add(DefaultGoPluginActivator.class, InnerClassStrategy.ALL) .set(Constants.BUNDLE_ACTIVATOR, DefaultGoPluginActivator.class.getCanonicalName()) .set(Constants.BUNDLE_CLASSPATH, ".,lib/dependency.jar") .set(Constants.BUNDLE_SYMBOLICNAME, GO_TEST_DUMMY_SYMBOLIC_NAME); for (Class aClass : classesToBeAdded) { bundleBeingBuilt.add(aClass, InnerClassStrategy.NONE); } ZipInputStream src = new ZipInputStream(bundleBeingBuilt.build()); File bundleExplodedDir = explodeBundleIntoDirectory(src, destinationDir); IOUtils.closeQuietly(src); return bundleExplodedDir; } private File explodeBundleIntoDirectory(ZipInputStream src, String destinationDir) throws IOException, URISyntaxException { File destinationPluginBundleLocation = new File(TMP_DIR, destinationDir); destinationPluginBundleLocation.mkdirs(); new ZipUtil().unzip(src, destinationPluginBundleLocation); return destinationPluginBundleLocation; } private class StubOfDefaultPluginRegistry extends DefaultPluginRegistry { public void fakeRegistrationOfPlugin(GoPluginDescriptor pluginDescriptor) { idToDescriptorMap.putIfAbsent(pluginDescriptor.id(), pluginDescriptor); } } } @Extension class DummyPluginAwareExtensionWhichIsNotPublic implements PluginDescriptorAware { @Override public void setPluginDescriptor(PluginDescriptor descriptor) { throw new UnsupportedOperationException(); } } class PackageLevelClassWithPublicInnerClass { @Extension public class DummyInnerClassWithExtension implements PluginDescriptorAware { @Override public void setPluginDescriptor(PluginDescriptor descriptor) { throw new UnsupportedOperationException(); } } }