/* * 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.infra; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.Dictionary; import java.util.List; import com.thoughtworks.go.util.SystemEnvironment; import com.thoughtworks.go.plugin.infra.plugininfo.GoPluginDescriptor; import com.thoughtworks.go.plugin.infra.plugininfo.PluginRegistry; import com.thoughtworks.go.plugin.infra.service.DefaultPluginHealthService; import com.thoughtworks.go.plugin.infra.service.DefaultPluginLoggingService; import com.thoughtworks.go.plugin.internal.api.LoggingService; import com.thoughtworks.go.plugin.internal.api.PluginHealthService; import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; import org.mockito.Matchers; import org.mockito.Mock; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; import org.osgi.framework.launch.Framework; import org.osgi.framework.launch.FrameworkFactory; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; public class FelixGoPluginOSGiFrameworkTest { public static final String TEST_SYMBOLIC_NAME = "testplugin.descriptorValidator"; private final GoPluginDescriptor descriptor = buildExpectedDescriptor(); @Mock private BundleContext bundleContext; @Mock private Bundle bundle; @Mock private Framework framework; @Mock private PluginRegistry registry; @Mock private SystemEnvironment systemEnvironment; private FelixGoPluginOSGiFramework spy; @Before public void setUp() throws Exception { initMocks(this); FelixGoPluginOSGiFramework goPluginOSGiFramwork = new FelixGoPluginOSGiFramework(registry, systemEnvironment); spy = spy(goPluginOSGiFramwork); when(framework.getBundleContext()).thenReturn(bundleContext); when(registry.getPlugin(TEST_SYMBOLIC_NAME)).thenReturn(descriptor); doReturn(framework).when(spy).getFelixFramework(Matchers.<List<FrameworkFactory>>anyObject()); } @Test public void shouldRegisterAnInstanceOfEachOfTheRequiredPluginServicesAfterOSGiFrameworkIsInitialized() { spy.start(); verify(bundleContext).registerService(eq(PluginHealthService.class), any(DefaultPluginHealthService.class), isNull(Dictionary.class)); verify(bundleContext).registerService(eq(LoggingService.class), any(DefaultPluginLoggingService.class), isNull(Dictionary.class)); } @Test public void shouldRunAnActionOnAllRegisteredImplementationsOfAGivenInterface() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); registerServices(firstService, secondService); spy.start(); spy.doOnAll(SomeInterface.class, new Action<SomeInterface>() { public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { obj.someMethod(); assertThat(pluginDescriptor, is(descriptor)); } }); verify(firstService).someMethod(); verify(secondService).someMethod(); verifyNoMoreInteractions(firstService, secondService); } @Test public void shouldFailWithAnExceptionWhenAnExceptionHandlerIsNotProvided() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); SomeInterface thirdService = mock(SomeInterface.class); registerServices(firstService, secondService, thirdService); spy.start(); RuntimeException exceptionToBeThrown = new RuntimeException("Ouch!"); doThrow(exceptionToBeThrown).when(secondService).someMethod(); try { spy.doOnAll(SomeInterface.class, new Action<SomeInterface>() { public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { obj.someMethod(); assertThat(pluginDescriptor, is(descriptor)); } }); } catch (RuntimeException e) { assertThat(e.getMessage(), is("Ouch!")); assertThat(e.getCause().getMessage(), is("Ouch!")); } verify(firstService).someMethod(); verify(secondService).someMethod(); verifyZeroInteractions(thirdService); verifyNoMoreInteractions(firstService, secondService); } @Test public void shouldAllowHandlingExceptionsDuringRunningOfAnActionOnAllRegisteredImplementationsOfAGivenInterface() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); SomeInterface thirdService = mock(SomeInterface.class); registerServices(firstService, secondService, thirdService); spy.start(); RuntimeException exceptionToBeThrown = new RuntimeException("Ouch!"); ExceptionHandler<SomeInterface> exceptionHandler = mock(ExceptionHandler.class); doThrow(exceptionToBeThrown).when(secondService).someMethod(); spy.doOnAllWithExceptionHandling(SomeInterface.class, new Action<SomeInterface>() { public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { obj.someMethod(); assertThat(pluginDescriptor, is(descriptor)); } }, exceptionHandler); InOrder inOrder = inOrder(firstService, secondService, thirdService, exceptionHandler); inOrder.verify(firstService).someMethod(); inOrder.verify(secondService).someMethod(); inOrder.verify(exceptionHandler).handleException(secondService, exceptionToBeThrown); inOrder.verify(thirdService).someMethod(); verifyNoMoreInteractions(exceptionHandler, firstService, secondService, thirdService); } @Test public void shouldDoNothingWhenTryingToRunOnAllImplementationsIfPluginsAreNotEnabled() throws Exception { SomeInterface firstService = mock(SomeInterface.class); registerServices(firstService); spy.doOnAll(SomeInterface.class, new Action<SomeInterface>() { public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { obj.someMethod(); assertThat(pluginDescriptor, is(descriptor)); } }); verifyZeroInteractions(firstService); ExceptionHandler exceptionHandler = mock(ExceptionHandler.class); spy.doOnAllWithExceptionHandling(SomeInterface.class, new Action<SomeInterface>() { public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { obj.someMethod(); } }, exceptionHandler); verifyZeroInteractions(firstService, exceptionHandler); } @Test public void doOnShouldRunAnActionOnSpecifiedPluginImplementationsOfAGivenInterface() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); registerServices(firstService, secondService); spy.start(); spy.doOn(SomeInterface.class, secondService.toString(), new ActionWithReturn<SomeInterface, Object>() { @Override public Object execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); return obj.someMethodWithReturn(); } }); spy.doOn(SomeInterface.class, secondService.toString(), new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); } }); spy.doOnWithExceptionHandling(SomeInterface.class, secondService.toString(), new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); } }, new ExceptionHandler<SomeInterface>() { @Override public void handleException(SomeInterface obj, Throwable t) { } } ); verify(firstService, never()).someMethodWithReturn(); verify(secondService).someMethodWithReturn(); verify(secondService, times(2)).someMethod(); verifyNoMoreInteractions(firstService, secondService); } @Test public void doOnExceptionHandlingShouldRunAnActionOnSpecifiedPluginImplementationsOfAGivenInterfaceAndDelegateTheExceptionToTheHandler() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); registerServices(firstService, secondService); spy.start(); final RuntimeException expectedException = new RuntimeException("Exception Thrown By Spy Method"); spy.doOnWithExceptionHandling(SomeInterface.class, secondService.toString(), new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); throw expectedException; } }, new ExceptionHandler<SomeInterface>() { @Override public void handleException(SomeInterface obj, Throwable t) { assertThat(t, is(expectedException)); } } ); verify(firstService, never()).someMethodWithReturn(); verify(secondService, never()).someMethodWithReturn(); verify(secondService).someMethod(); verifyNoMoreInteractions(firstService, secondService); } @Test public void doOnShouldThrowAnExceptionWhenThereAreMultipleServicesWithSamePluginId_IdeallyThisShouldNotHappenInProductionSincePluginIdIsSymbolicName() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); String symbolicName = "same_symbolic_name"; registerServicesWithSameSymbolicName(symbolicName, firstService, secondService); spy.start(); try { spy.doOn(SomeInterface.class, symbolicName, new ActionWithReturn<SomeInterface, Object>() { @Override public Object execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); return obj.someMethodWithReturn(); } }); fail("Should throw plugin framework exception"); } catch (GoPluginFrameworkException ex) { assertThat(ex.getMessage().startsWith("More than one reference found"), is(true)); assertThat(ex.getMessage().contains(SomeInterface.class.getCanonicalName()), is(true)); assertThat(ex.getMessage().contains(symbolicName), is(true)); } try { spy.doOn(SomeInterface.class, symbolicName, new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); } }); fail("Should throw plugin framework exception"); } catch (GoPluginFrameworkException ex) { assertThat(ex.getMessage().startsWith("More than one reference found"), is(true)); assertThat(ex.getMessage().contains(SomeInterface.class.getCanonicalName()), is(true)); assertThat(ex.getMessage().contains(symbolicName), is(true)); } try { spy.doOnWithExceptionHandling(SomeInterface.class, symbolicName, new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); } }, new ExceptionHandler<SomeInterface>() { @Override public void handleException(SomeInterface obj, Throwable t) { } } ); fail("Should throw plugin framework exception"); } catch (GoPluginFrameworkException ex) { assertThat(ex.getMessage().startsWith("More than one reference found"), is(true)); assertThat(ex.getMessage().contains(SomeInterface.class.getCanonicalName()), is(true)); assertThat(ex.getMessage().contains(symbolicName), is(true)); } verify(firstService, never()).someMethodWithReturn(); verify(secondService, never()).someMethodWithReturn(); verify(secondService, never()).someMethod(); verifyNoMoreInteractions(firstService, secondService); } @Test public void doOnShouldThrowAnExceptionWhenThereAreNoServicesAreFoundForTheGivenFilterAndServiceReference() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); String symbolicName = "dummy_symbolic_name"; registerServicesWithSameSymbolicName(symbolicName, firstService, secondService); spy.start(); try { spy.doOn(SomeOtherInterface.class, symbolicName, new ActionWithReturn<SomeOtherInterface, Object>() { @Override public Object execute(SomeOtherInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); throw new RuntimeException("Should Not Be invoked"); } }); fail("Should throw plugin framework exception"); } catch (GoPluginFrameworkException ex) { assertThat(ex.getMessage().startsWith("No reference found"), is(true)); assertThat(ex.getMessage().contains(SomeOtherInterface.class.getCanonicalName()), is(true)); assertThat(ex.getMessage().contains(symbolicName), is(true)); } try { spy.doOn(SomeOtherInterface.class, symbolicName, new Action<SomeOtherInterface>() { @Override public void execute(SomeOtherInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); throw new RuntimeException("Should Not Be invoked"); } }); fail("Should throw plugin framework exception"); } catch (GoPluginFrameworkException ex) { assertThat(ex.getMessage().startsWith("No reference found"), is(true)); assertThat(ex.getMessage().contains(SomeOtherInterface.class.getCanonicalName()), is(true)); assertThat(ex.getMessage().contains(symbolicName), is(true)); } try { spy.doOnWithExceptionHandling(SomeOtherInterface.class, symbolicName, new Action<SomeOtherInterface>() { @Override public void execute(SomeOtherInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); throw new RuntimeException("Should Not Be invoked"); } }, new ExceptionHandler<SomeOtherInterface>() { @Override public void handleException(SomeOtherInterface obj, Throwable t) { } } ); fail("Should throw plugin framework exception"); } catch (GoPluginFrameworkException ex) { assertThat(ex.getMessage().startsWith("No reference found"), is(true)); assertThat(ex.getMessage().contains(SomeOtherInterface.class.getCanonicalName()), is(true)); assertThat(ex.getMessage().contains(symbolicName), is(true)); } verify(firstService, never()).someMethodWithReturn(); verify(secondService, never()).someMethodWithReturn(); verify(secondService, never()).someMethod(); verifyNoMoreInteractions(firstService, secondService); } @Test public void doOnAllShouldRunAnActionOnAllPluginExtensionsOfAGivenPluginJar() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); String symbolicName = "same_symbolic_name"; registerServicesWithSameSymbolicName(symbolicName, firstService, secondService); spy.start(); spy.doOnAllForPlugin(SomeInterface.class, symbolicName, new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); } }); verify(secondService).someMethod(); verify(firstService).someMethod(); verifyNoMoreInteractions(firstService, secondService); } @Test public void doOnAllWithExceptionHandlingShouldRunAnActionOnAllPluginExtensionsOfAGivenPluginJar() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); String symbolicName = "same_symbolic_name"; registerServicesWithSameSymbolicName(symbolicName, firstService, secondService); spy.start(); spy.doOnAllWithExceptionHandlingForPlugin(SomeInterface.class, symbolicName, new Action<SomeInterface>() { @Override public void execute(SomeInterface obj, GoPluginDescriptor pluginDescriptor) { assertThat(pluginDescriptor, is(descriptor)); obj.someMethod(); throw new RuntimeException("Dummy Exception"); } }, new ExceptionHandler<SomeInterface>() { @Override public void handleException(SomeInterface obj, Throwable t) { } } ); verify(secondService).someMethod(); verify(firstService).someMethod(); verifyNoMoreInteractions(firstService, secondService); } private void registerServicesWithSameSymbolicName(String symbolicName, SomeInterface... someInterfaces) throws InvalidSyntaxException { ArrayList<ServiceReference<SomeInterface>> references = new ArrayList<>(); for (int i = 0; i < someInterfaces.length; ++i) { ServiceReference reference = mock(ServiceReference.class); Bundle bundle = mock(Bundle.class); when(reference.getBundle()).thenReturn(bundle); when(bundle.getSymbolicName()).thenReturn(TEST_SYMBOLIC_NAME); when(bundleContext.getService(reference)).thenReturn(someInterfaces[i]); references.add(reference); } String propertyFormat = String.format("(%s=%s)", Constants.BUNDLE_SYMBOLICNAME, symbolicName); when(bundleContext.getServiceReferences(SomeInterface.class, propertyFormat)).thenReturn(references); } @Test public void HasReferencesShouldReturnAppropriateValueIfSpecifiedPluginImplementationsOfAGivenInterfaceIsFoundOrNotFound() throws Exception { SomeInterface firstService = mock(SomeInterface.class); SomeInterface secondService = mock(SomeInterface.class); spy.start(); boolean reference = spy.hasReferenceFor(SomeInterface.class, secondService.toString()); assertThat(reference, is(false)); registerServices(firstService, secondService); reference = spy.hasReferenceFor(SomeInterface.class, secondService.toString()); assertThat(reference, is(true)); verifyNoMoreInteractions(firstService, secondService); } @Test public void shouldUnloadAPlugin() throws BundleException { GoPluginDescriptor pluginDescriptor = mock(GoPluginDescriptor.class); Bundle bundle = mock(Bundle.class); when(pluginDescriptor.bundle()).thenReturn(bundle); spy.unloadPlugin(pluginDescriptor); verify(bundle, atLeastOnce()).stop(); verify(bundle, atLeastOnce()).uninstall(); } @Test public void shouldUnloadAnInvalidPlugin() throws BundleException { GoPluginDescriptor pluginDescriptor = mock(GoPluginDescriptor.class); Bundle bundle = mock(Bundle.class); when(pluginDescriptor.bundle()).thenReturn(bundle); when(pluginDescriptor.isInvalid()).thenReturn(true); spy.unloadPlugin(pluginDescriptor); verify(bundle, atLeastOnce()).stop(); verify(bundle, atLeastOnce()).uninstall(); } @Test public void shouldNotUnloadBundleForAnUnloadedInvalidPlugin() throws BundleException { GoPluginDescriptor pluginDescriptor = mock(GoPluginDescriptor.class); when(pluginDescriptor.bundle()).thenReturn(null); spy.unloadPlugin(pluginDescriptor); } @Test public void shouldMarkThePluginAsInvalidIfAnyExceptionOccursAfterLoad() throws BundleException { final Bundle bundle = mock(Bundle.class); spy.addPluginChangeListener(new PluginChangeListener() { @Override public void pluginLoaded(GoPluginDescriptor pluginDescriptor) { throw new RuntimeException("some error"); } @Override public void pluginUnLoaded(GoPluginDescriptor pluginDescriptor) { } }); when(bundleContext.installBundle(any(String.class))).thenReturn(bundle); final GoPluginDescriptor goPluginDescriptor = new GoPluginDescriptor(TEST_SYMBOLIC_NAME, "1.0", null, "location", new File(""), false); spy.start(); try { spy.loadPlugin(goPluginDescriptor); fail("should throw exception"); } catch (Exception e) { assertTrue(goPluginDescriptor.getStatus().isInvalid()); } } private void registerServices(SomeInterface... someInterfaces) throws InvalidSyntaxException { ArrayList<ServiceReference<SomeInterface>> references = new ArrayList<>(); for (int i = 0; i < someInterfaces.length; ++i) { ServiceReference reference = mock(ServiceReference.class); when(reference.getBundle()).thenReturn(bundle); when(bundle.getSymbolicName()).thenReturn(TEST_SYMBOLIC_NAME); when(bundleContext.getService(reference)).thenReturn(someInterfaces[i]); setExpectationForFilterBasedServiceReferenceCall(someInterfaces[i], reference); references.add(reference); } when(bundleContext.getServiceReferences(SomeInterface.class, null)).thenReturn(references); } private void setExpectationForFilterBasedServiceReferenceCall(SomeInterface service, ServiceReference reference) throws InvalidSyntaxException { ArrayList<ServiceReference<SomeInterface>> references = new ArrayList<>(); String propertyFormat = String.format("(%s=%s)", Constants.BUNDLE_SYMBOLICNAME, service.toString()); references.add(reference); when(bundleContext.getServiceReferences(SomeInterface.class, propertyFormat)).thenReturn(references); } private GoPluginDescriptor buildExpectedDescriptor() { return new GoPluginDescriptor(TEST_SYMBOLIC_NAME, "1", new GoPluginDescriptor.About("Plugin Descriptor Validator", "1.0.1", "12.4", "Validates its own plugin descriptor", new GoPluginDescriptor.Vendor("ThoughtWorks Go Team", "www.thoughtworks.com"), Arrays.asList("Linux", "Windows")), null, null, true ); } private interface SomeInterface { void someMethod(); Object someMethodWithReturn(); } private interface SomeOtherInterface { } }