/* * 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.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.api.logging.Logger; import com.thoughtworks.go.plugin.internal.api.LoggingService; import com.thoughtworks.go.plugin.internal.api.PluginHealthService; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import java.net.MalformedURLException; import java.net.URL; import java.util.Enumeration; import java.util.Hashtable; import java.util.List; import static java.lang.String.format; import static java.util.Arrays.asList; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.contains; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; import static org.mockito.MockitoAnnotations.initMocks; public class DefaultGoPluginActivatorTest { private static final String CONSTRUCTOR_FAIL_MSG = "Ouch! Failed construction"; private static final String PLUGIN_ID = "plugin-id"; public static final String NO_EXT_ERR_MSG = "No extensions found in this plugin.Please check for @Extension annotations"; private DefaultGoPluginActivator activator; @Mock private BundleContext context; @Mock private Bundle bundle; @Mock private ServiceReference<PluginHealthService> pluginHealthServiceReference; @Mock private PluginHealthService pluginHealthService; @Mock private ServiceReference<LoggingService> loggingServiceReference; @Mock private LoggingService loggingService; private Enumeration<URL> emptyListOfClassesInBundle = new Hashtable<URL, String>().keys(); @Before public void setUp() { initMocks(this); when(context.getServiceReference(PluginHealthService.class)).thenReturn(pluginHealthServiceReference); when(context.getServiceReference(LoggingService.class)).thenReturn(loggingServiceReference); when(context.getService(pluginHealthServiceReference)).thenReturn(pluginHealthService); when(context.getService(loggingServiceReference)).thenReturn(loggingService); when(context.getBundle()).thenReturn(bundle); when(bundle.getSymbolicName()).thenReturn(PLUGIN_ID); when(bundle.findEntries("/", "*.class", true)).thenReturn(emptyListOfClassesInBundle); activator = new DefaultGoPluginActivator(); } @After public void tearDown() throws Exception { Logger.initialize(null); } @Test public void shouldReportAClassLoadErrorToThePluginHealthService() throws Exception { setupClassesInBundle("SomeClass.class"); when(bundle.loadClass(anyString())).thenThrow(new ClassNotFoundException("Ouch! Failed")); activator.start(context); verifyErrorsReported("Class [SomeClass] could not be loaded. Message: [Ouch! Failed].", NO_EXT_ERR_MSG); } @Test public void shouldReportMultipleClassLoadErrorsToThePluginHealthService() throws Exception { setupClassesInBundle("SomeClass.class", "SomeOtherClass.class"); when(bundle.loadClass(anyString())).thenThrow(new ClassNotFoundException("Ouch! Failed")); activator.start(context); verifyErrorsReported("Class [SomeClass] could not be loaded. Message: [Ouch! Failed].", "Class [SomeOtherClass] could not be loaded. Message: [Ouch! Failed]." , NO_EXT_ERR_MSG); } @Test public void shouldReportAClassWhichIsAnnotatedAsAnExtensionIfItIsNotPublic() throws Exception { setupClassesInBundle("NonPublicGoExtensionClass.class"); when(bundle.loadClass(contains("NonPublicGoExtensionClass"))).thenReturn((Class) NonPublicGoExtensionClass.class); activator.start(context); verifyErrorsReported("Class [NonPublicGoExtensionClass] is annotated with @Extension but is not public.", NO_EXT_ERR_MSG); } @Test public void shouldNotReportAClassWhichIsNotAnnotatedAsAnExtensionEvenIfItIsNotPublic() throws Exception { setupClassesInBundle("NonPublicClassWhichIsNotAGoExtension.class"); when(bundle.loadClass(contains("NonPublicClassWhichIsNotAGoExtension"))).thenReturn((Class) NonPublicClassWhichIsNotAGoExtension.class); activator.start(context); verifyErrorsReported(NO_EXT_ERR_MSG); } @Test public void shouldReportAClassWhichIsAnnotatedAsAnExtensionIfItIsAbstract() throws Exception { setupClassesInBundle("PublicAbstractGoExtensionClass.class"); when(bundle.loadClass(contains("PublicAbstractGoExtensionClass"))).thenReturn((Class) PublicAbstractGoExtensionClass.class); activator.start(context); verifyErrorsReported("Class [PublicAbstractGoExtensionClass] is annotated with @Extension but is abstract.", NO_EXT_ERR_MSG); } @Test public void shouldReportAClassWhichIsAnnotatedAsAnExtensionIfItIsNotInstantiable() throws Exception { setupClassesInBundle("PublicGoExtensionClassWhichDoesNotHaveADefaultConstructor.class"); when(bundle.loadClass(contains("PublicGoExtensionClassWhichDoesNotHaveADefaultConstructor"))).thenReturn((Class) PublicGoExtensionClassWhichDoesNotHaveADefaultConstructor.class); activator.start(context); verifyErrorsReported("Class [PublicGoExtensionClassWhichDoesNotHaveADefaultConstructor] is annotated with @Extension but cannot be constructed. " + "Make sure it and all of its parent classes have a default constructor.", NO_EXT_ERR_MSG); } @Test public void shouldReportAClassWhichIsAnnotatedAsAnExtensionIfItFailsDuringConstruction() throws Exception { setupClassesInBundle("PublicGoExtensionClassWhichThrowsAnExceptionInItsConstructor.class"); when(bundle.loadClass(contains("PublicGoExtensionClassWhichThrowsAnExceptionInItsConstructor"))).thenReturn((Class) PublicGoExtensionClassWhichThrowsAnExceptionInItsConstructor.class); activator.start(context); verifyErrorsReported( format("Class [PublicGoExtensionClassWhichThrowsAnExceptionInItsConstructor] is annotated with @Extension but cannot be constructed. Reason: java.lang.RuntimeException: %s.", CONSTRUCTOR_FAIL_MSG), NO_EXT_ERR_MSG); } @Test public void shouldSetupTheLoggerWithTheLoggingServiceAndPluginId() throws Exception { setupClassesInBundle(); activator.start(context); Logger logger = Logger.getLoggerFor(DefaultGoPluginActivatorTest.class); logger.info("INFO"); verify(loggingService).info(PLUGIN_ID, DefaultGoPluginActivatorTest.class.getName(), "INFO"); } @Test public void loggerShouldBeAvailableToBeUsedInStaticBlocksAndConstructorAndLoadUnloadMethodsOfPluginExtensionClasses() throws Exception { setupClassesInBundle("PublicGoExtensionClassWhichLogsInAStaticBlock.class"); when(bundle.loadClass(contains("PublicGoExtensionClassWhichLogsInAStaticBlock"))).thenReturn((Class) PublicGoExtensionClassWhichLogsInAStaticBlock.class); activator.start(context); activator.stop(context); verify(loggingService).info(PLUGIN_ID, PublicGoExtensionClassWhichLogsInAStaticBlock.class.getName(), "HELLO from static block in PublicGoExtensionClassWhichLogsInAStaticBlock"); verify(loggingService).info(PLUGIN_ID, PublicGoExtensionClassWhichLogsInAStaticBlock.class.getName(), "HELLO from constructor in PublicGoExtensionClassWhichLogsInAStaticBlock"); verify(loggingService).info(PLUGIN_ID, PublicGoExtensionClassWhichLogsInAStaticBlock.class.getName(), "HELLO from load in PublicGoExtensionClassWhichLogsInAStaticBlock"); verify(loggingService).info(PLUGIN_ID, PublicGoExtensionClassWhichLogsInAStaticBlock.class.getName(), "HELLO from unload in PublicGoExtensionClassWhichLogsInAStaticBlock"); } @Test public void shouldInvokeMethodWithLoadUnloadAnnotationAtPluginStart() throws Exception { setupClassesInBundle("GoExtensionWithLoadUnloadAnnotation.class"); when(bundle.loadClass(contains("GoExtensionWithLoadUnloadAnnotation"))).thenReturn((Class) GoExtensionWithLoadUnloadAnnotation.class); activator.start(context); assertThat(GoExtensionWithLoadUnloadAnnotation.loadInvoked, is(1)); activator.stop(context); assertThat(GoExtensionWithLoadUnloadAnnotation.unLoadInvoked, is(1)); } @Test public void shouldNotInvokeMethodWithLoadUnloadAnnotationAtPluginStartIfTheClassIsNotAnExtension() throws Exception { assertDidNotInvokeLoadUnload(NonExtensionWithLoadUnloadAnnotation.class); } @Test public void shouldNotInvokeStaticMethodWithLoadAnnotationAtPluginStart() throws Exception { assertDidNotInvokeLoadUnload(GoExtensionWithStaticLoadAnnotationMethod.class); } @Test public void shouldNotInvokeNonPublicMethodWithLoadAnnotationAtPluginStart() throws Exception { assertDidNotInvokeLoadUnload(GoExtensionWithNonPublicLoadUnloadAnnotation.class); } @Test public void shouldNotInvokePublicMethodWithLoadAnnotationHavingArgumentsAtPluginStart() throws Exception { assertDidNotInvokeLoadUnload(GoExtensionWithPublicLoadUnloadAnnotationWithArguments.class); } @Test public void shouldNotInvokeInheritedPublicMethodWithLoadAnnotationAtPluginStart() throws Exception { assertDidNotInvokeLoadUnload(GoExtensionWithInheritedPublicLoadUnloadAnnotationMethod.class); } private void assertDidNotInvokeLoadUnload(Class<?> testExtensionClass) throws Exception { assertLoadUnloadInvocationCount(testExtensionClass, 0); } private void assertLoadUnloadInvocationCount(Class<?> testExtensionClass, int invocationCount) throws Exception { String simpleNameOfTestExtensionClass = testExtensionClass.getSimpleName(); setupClassesInBundle(simpleNameOfTestExtensionClass + ".class"); when(bundle.loadClass(contains(simpleNameOfTestExtensionClass))).thenReturn((Class) testExtensionClass); activator.start(context); assertThat(testExtensionClass.getField("loadInvoked").getInt(null), is(invocationCount)); activator.stop(context); assertThat(testExtensionClass.getField("unLoadInvoked").getInt(null), is(invocationCount)); } @Test public void shouldGenerateExceptionWhenThereAreMoreThanOneLoadAnnotationsAtPluginStart() throws Exception { String expectedErrorMessageWithMethodsWithIncreasingOrder = "Class [GoExtensionWithMultipleLoadUnloadAnnotation] is annotated with @Extension will not be registered. " + "Reason: java.lang.RuntimeException: More than one method with @Load annotation not allowed. " + "Methods Found: [public void com.thoughtworks.go.plugin.activation.GoExtensionWithMultipleLoadUnloadAnnotation.setupData1(com.thoughtworks.go.plugin.api.info.PluginContext), " + "public void com.thoughtworks.go.plugin.activation.GoExtensionWithMultipleLoadUnloadAnnotation.setupData2(com.thoughtworks.go.plugin.api.info.PluginContext)]."; String expectedErrorMessageWithMethodsWithDecreasingOrder = "Class [GoExtensionWithMultipleLoadUnloadAnnotation] is annotated with @Extension will not be registered. " + "Reason: java.lang.RuntimeException: More than one method with @Load annotation not allowed. " + "Methods Found: [public void com.thoughtworks.go.plugin.activation.GoExtensionWithMultipleLoadUnloadAnnotation.setupData2(com.thoughtworks.go.plugin.api.info.PluginContext), " + "public void com.thoughtworks.go.plugin.activation.GoExtensionWithMultipleLoadUnloadAnnotation.setupData1(com.thoughtworks.go.plugin.api.info.PluginContext)]."; setupClassesInBundle("GoExtensionWithMultipleLoadUnloadAnnotation.class"); when(bundle.loadClass(contains("GoExtensionWithMultipleLoadUnloadAnnotation"))).thenReturn((Class) GoExtensionWithMultipleLoadUnloadAnnotation.class); activator.start(context); assertThat(activator.hasErrors(), is(true)); verifyThatOneOfTheErrorMessagesIsPresent(expectedErrorMessageWithMethodsWithIncreasingOrder, expectedErrorMessageWithMethodsWithDecreasingOrder); activator.stop(context); verifyThatOneOfTheErrorMessagesIsPresent(expectedErrorMessageWithMethodsWithIncreasingOrder, expectedErrorMessageWithMethodsWithDecreasingOrder); } @Test public void shouldHandleExceptionGeneratedByLoadMethodAtPluginStart() throws Exception { setupClassesInBundle("GoExtensionWithLoadAnnotationMethodThrowingException.class"); when(bundle.loadClass(contains("GoExtensionWithLoadAnnotationMethodThrowingException"))).thenReturn((Class) GoExtensionWithLoadAnnotationMethodThrowingException.class); activator.start(context); assertThat(activator.hasErrors(), is(true)); verifyErrorsReported("Class [GoExtensionWithLoadAnnotationMethodThrowingException] is annotated with @Extension but cannot be registered. " + "Reason: java.io.IOException: Load Dummy Checked Exception."); } @Test public void shouldHandleExceptionGeneratedByUnLoadMethodAtPluginStop() throws Exception { setupClassesInBundle("GoExtensionWithUnloadAnnotationMethodThrowingException.class"); when(bundle.loadClass(contains("GoExtensionWithUnloadAnnotationMethodThrowingException"))).thenReturn((Class) GoExtensionWithUnloadAnnotationMethodThrowingException.class); activator.start(context); assertThat(activator.hasErrors(), is(false)); activator.stop(context); assertThat(activator.hasErrors(), is(true)); verifyErrorsReported("Invocation of unload method [public int com.thoughtworks.go.plugin.activation.GoExtensionWithUnloadAnnotationMethodThrowingException" + ".throwExceptionAgain(com.thoughtworks.go.plugin.api.info.PluginContext) " + "throws java.io.IOException]. " + "Reason: java.io.IOException: Unload Dummy Checked Exception."); } private void verifyThatOneOfTheErrorMessagesIsPresent(String expectedErrorMessage1, String expectedErrorMessage2) { ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class); verify(pluginHealthService).reportErrorAndInvalidate(eq(PLUGIN_ID), captor.capture()); verifyNoMoreInteractions(pluginHealthService); String actualErrorMessage = (String) captor.getValue().get(0); assertTrue(expectedErrorMessage1.equals(actualErrorMessage) || expectedErrorMessage2.equals(actualErrorMessage)); } private void setupClassesInBundle(String... classes) throws MalformedURLException, ClassNotFoundException { Hashtable<URL, String> classFileEntries = new Hashtable<>(); for (String aClass : classes) { classFileEntries.put(new URL("file:///" + aClass), ""); } when(bundle.findEntries("/", "*.class", true)).thenReturn(classFileEntries.keys()); } private void verifyErrorsReported(String... errors) { verify(pluginHealthService).reportErrorAndInvalidate(PLUGIN_ID, asList(errors)); verifyNoMoreInteractions(pluginHealthService); } @Extension public abstract class PublicAbstractGoExtensionClass { } @Extension public class PublicGoExtensionClassWhichDoesNotHaveADefaultConstructor implements PluginDescriptorAware { public PublicGoExtensionClassWhichDoesNotHaveADefaultConstructor(int x) { } @Override public void setPluginDescriptor(PluginDescriptor descriptor) { } } @Extension public class PublicGoExtensionClassWhichThrowsAnExceptionInItsConstructor implements PluginDescriptorAware { public PublicGoExtensionClassWhichThrowsAnExceptionInItsConstructor() { throw new RuntimeException(CONSTRUCTOR_FAIL_MSG); } @Override public void setPluginDescriptor(PluginDescriptor descriptor) { } } } @Extension class NonPublicGoExtensionClass { } class NonPublicClassWhichIsNotAGoExtension { }