View Javadoc

1   package com.atlassian.sal.core.lifecycle;
2   
3   import java.util.Collection;
4   import java.util.Hashtable;
5   
6   import com.atlassian.plugin.Plugin;
7   import com.atlassian.plugin.PluginAccessor;
8   import com.atlassian.plugin.PluginController;
9   import com.atlassian.plugin.event.PluginEventManager;
10  import com.atlassian.plugin.event.events.PluginEnabledEvent;
11  import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
12  import com.atlassian.plugin.osgi.factory.OsgiPlugin;
13  import com.atlassian.sal.api.lifecycle.LifecycleAware;
14  
15  import com.google.common.collect.ImmutableList;
16  import com.google.common.collect.ImmutableMap;
17  
18  import org.junit.Before;
19  import org.junit.Test;
20  import org.junit.runner.RunWith;
21  import org.mockito.Mock;
22  import org.mockito.invocation.InvocationOnMock;
23  import org.mockito.runners.MockitoJUnitRunner;
24  import org.mockito.stubbing.Answer;
25  import org.osgi.framework.Bundle;
26  import org.osgi.framework.BundleContext;
27  import org.osgi.framework.ServiceEvent;
28  import org.osgi.framework.ServiceListener;
29  import org.osgi.framework.ServiceReference;
30  
31  import static org.hamcrest.MatcherAssert.assertThat;
32  import static org.hamcrest.Matchers.is;
33  import static org.hamcrest.Matchers.notNullValue;
34  import static org.mockito.Matchers.any;
35  import static org.mockito.Matchers.anyString;
36  import static org.mockito.Matchers.same;
37  import static org.mockito.Mockito.doAnswer;
38  import static org.mockito.Mockito.doThrow;
39  import static org.mockito.Mockito.mock;
40  import static org.mockito.Mockito.never;
41  import static org.mockito.Mockito.verify;
42  import static org.mockito.Mockito.when;
43  
44  @RunWith(MockitoJUnitRunner.class)
45  public class TestDefaultLifecycleManager
46  {
47      private final String pluginKey = "pluginKey";
48  
49      @Mock private PluginEventManager pluginEventManager;
50      @Mock private PluginAccessor pluginAccessor;
51      @Mock private BundleContext bundleContext;
52  
53      @Mock private Bundle pluginBundle;
54      @Mock private PluginController pluginController;
55      private ServiceListener serviceListener;
56  
57      private boolean isApplicationSetup;
58      private int notifyOnStartCalled;
59      private DefaultLifecycleManager defaultLifecycleManager;
60  
61      @Before
62      public void setUp() throws Exception
63      {
64          // Setup object under test
65  
66          isApplicationSetup = false;
67          defaultLifecycleManager = new DefaultLifecycleManager(pluginEventManager, pluginAccessor, bundleContext)
68          {
69              @Override
70              public boolean isApplicationSetUp()
71              {
72                  return isApplicationSetup;
73              }
74  
75              @Override
76              protected void notifyOnStart()
77              {
78                  super.notifyOnStart();
79                  ++notifyOnStartCalled;
80              }
81          };
82  
83          // Setup recurrent fixtures
84  
85          pluginBundle = mockBundle(pluginKey);
86          // We need to use an answer here so we get serviceListener set during the call to facilitate race condition tests, and for
87          // the same reason we can't use a vanilla ArgumentCaptor here (with polluting every serviceListener usage site).
88          final Answer answer = new Answer()
89          {
90              @Override
91              public Object answer(final InvocationOnMock invocation) throws Throwable
92              {
93                  serviceListener = (ServiceListener) invocation.getArguments()[0];
94                  return null;
95              }
96          };
97          doAnswer(answer).when(bundleContext).addServiceListener(any(ServiceListener.class), anyString());
98      }
99  
100     @Test
101     public void onStartCalledForServicesRegisteredBeforeDefaultLifecycleManagerIsInitialized() throws Exception
102     {
103         final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
104         final ServiceReference<LifecycleAware> service = registerService(pluginBundle, lifecycleAware);
105         when(bundleContext.getServiceReferences(LifecycleAware.class, null)).thenReturn(ImmutableList.of(service));
106 
107         defaultLifecycleManager.afterPropertiesSet();
108 
109         isApplicationSetup = true;
110         enablePlugin(pluginKey);
111 
112         // Should not be called yet, as things haven't started
113         verify(lifecycleAware, never()).onStart();
114 
115         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
116 
117         // And now it should have started
118         verify(lifecycleAware).onStart();
119 
120         // And this is a convenient test to check resource management. This isn't perfect (we should check calls are balanced),
121         // but it's not bad and picks up the likely broken code cases.
122         verify(bundleContext).ungetService(service);
123     }
124 
125     @Test
126     public void onStartCalledWhenPluginFrameworkStartedIfApplicationIsSetup() throws Exception
127     {
128         defaultLifecycleManager.afterPropertiesSet();
129 
130         final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
131         enablePlugin(pluginKey);
132 
133         // Should not be called yet, as things haven't started
134         verify(lifecycleAware, never()).onStart();
135 
136         isApplicationSetup = true;
137         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
138 
139         // And now it should have started
140         verify(lifecycleAware).onStart();
141     }
142 
143     @Test
144     public void onStartCalledWhenStartCalledIfApplicationIsSetupAfterPluginFrameworkStarted() throws Exception
145     {
146         defaultLifecycleManager.afterPropertiesSet();
147 
148         final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
149         enablePlugin(pluginKey);
150 
151         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
152 
153         // Should not be called yet, as things haven't started
154         verify(lifecycleAware, never()).onStart();
155 
156         isApplicationSetup = true;
157         defaultLifecycleManager.start();
158 
159         // And now it should have started
160         verify(lifecycleAware).onStart();
161     }
162 
163     @Test
164     public void onStartCalledWhenPluginEnabledAfterStarted() throws Exception
165     {
166         defaultLifecycleManager.afterPropertiesSet();
167 
168         final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
169         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
170         isApplicationSetup = true;
171         defaultLifecycleManager.start();
172 
173         // Should not be called yet, as plugin is not enabled
174         verify(lifecycleAware, never()).onStart();
175 
176         enablePlugin(pluginKey);
177 
178         // Should have been called now, in response to plugin being enabled
179         verify(lifecycleAware).onStart();
180     }
181 
182     @Test
183     public void onStartCalledWhenLifecycleAwareRegisteredAfterPluginEnabled() throws Exception
184     {
185         defaultLifecycleManager.afterPropertiesSet();
186 
187         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
188         isApplicationSetup = true;
189         defaultLifecycleManager.start();
190 
191         enablePlugin(pluginKey);
192 
193         final LifecycleAware lifecycleAware = mockLifecycleAwareAndRegisterService(pluginBundle);
194         // Should have been called now, since the system is started and the plugin is enabled
195         verify(lifecycleAware).onStart();
196     }
197 
198     @Test
199     public void notifyOnStartCalledWhenPluginFrameworkStartedWhenSetup() throws Exception
200     {
201         defaultLifecycleManager.afterPropertiesSet();
202 
203         isApplicationSetup = true;
204         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
205 
206         assertThat(notifyOnStartCalled, is(1));
207     }
208 
209     @Test
210     public void notifyOnStartCalledWhenStartCalledIfApplicationIsSetupAfterPluginFrameworkStarted() throws Exception
211     {
212         defaultLifecycleManager.afterPropertiesSet();
213 
214         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
215 
216         // Not yet setup, so we should not have started yet
217         assertThat(notifyOnStartCalled, is(0));
218 
219         isApplicationSetup = true;
220         defaultLifecycleManager.start();
221 
222         assertThat(notifyOnStartCalled, is(1));
223     }
224 
225     @Test
226     public void notifyOnStartOnlyCalledOnceEvenWhenBothPluginFrameworkStartedAndStartCalledWhenSetup() throws Exception
227     {
228         defaultLifecycleManager.afterPropertiesSet();
229 
230         isApplicationSetup = true;
231         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
232         defaultLifecycleManager.start();
233 
234         assertThat(notifyOnStartCalled, is(1));
235     }
236 
237     @Test
238     public void onStartNotCalledWhenLifecycleAwareUnregisteredBeforeStart() throws Exception
239     {
240         defaultLifecycleManager.afterPropertiesSet();
241         // This is the shape you see if a plugin fails to enable but does get some services up before being disabled.
242         final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
243         final ServiceReference serviceReference = registerService(pluginBundle, lifecycleAware);
244         unregisterService(serviceReference);
245 
246         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
247         isApplicationSetup = true;
248         defaultLifecycleManager.start();
249     }
250 
251     @Test
252     public void verifyListenerRegistrationManagement() throws Exception
253     {
254         defaultLifecycleManager.afterPropertiesSet();
255 
256         assertThat(serviceListener, notNullValue());
257 
258         verify(pluginEventManager).register(defaultLifecycleManager);
259         // This is a little smoke and mirrors, in that we just captured it, but i want to keep this test orthogonal, and it serves
260         // to document the listener registration management also.
261         verify(bundleContext).addServiceListener(same(serviceListener), anyString());
262 
263 
264         verify(pluginEventManager, never()).unregister(defaultLifecycleManager);
265         verify(bundleContext, never()).removeServiceListener(serviceListener);
266 
267         defaultLifecycleManager.destroy();
268 
269         verify(pluginEventManager).unregister(defaultLifecycleManager);
270         verify(bundleContext).removeServiceListener(serviceListener);
271     }
272 
273     @Test
274     public void serviceDeregisteredJustAfterPostConstructSnapshotIsNotLeaked() throws Exception
275     {
276         // This is a very greybox test, but i think there's logic worth exercising
277         final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
278         final ServiceReference<LifecycleAware> serviceReference = registerService(pluginBundle, lifecycleAware);
279         // Arrange that just after DefaultLifecycleManager.postConstruct asks for the references, one of them goes away.
280         when(bundleContext.getServiceReferences(LifecycleAware.class, null)).thenAnswer(
281                 new Answer<Collection<ServiceReference<LifecycleAware>>>()
282                 {
283                     @Override
284                     public Collection<ServiceReference<LifecycleAware>> answer(final InvocationOnMock invocation)
285                             throws Throwable
286                     {
287                         // The idea is that BundleContext.getServiceReferences return the one service, and then it unregistered
288                         final ImmutableList<ServiceReference<LifecycleAware>> services = ImmutableList.of(serviceReference);
289                         unregisterService(serviceReference);
290                         return services;
291                     }
292                 });
293 
294         defaultLifecycleManager.afterPropertiesSet();
295 
296         // This is kind of a bit kludgy in terms of the test scenario, but it's the only really reasonable way to arrange we'd
297         // detect the failure we're looking for. The alternative is to add debug logging for this scenario and capture logging.
298         enablePlugin(pluginKey);
299         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
300         isApplicationSetup = true;
301         defaultLifecycleManager.start();
302 
303         // We should not start the lifecyleAware, because it wasn't registered by the time we started.
304         verify(lifecycleAware, never()).onStart();
305     }
306 
307     @Test
308     public void onStartExceptionsAreCaught() throws Exception
309     {
310         // It would be even better if we could test other instances of LifecycleAware are unaffected also, but without playing
311         // crazy brittle games on the HashSet ordering, i don't see a way to make such a test reliable.
312         defaultLifecycleManager.afterPropertiesSet();
313 
314         final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
315         final ServiceReference<LifecycleAware> service = registerService(pluginBundle, lifecycleAware);
316         doThrow(new RuntimeException("Test onStart failure")).when(lifecycleAware).onStart();
317         enablePlugin(pluginKey);
318 
319         // Should not be called yet, as things haven't started
320         verify(lifecycleAware, never()).onStart();
321 
322         isApplicationSetup = true;
323         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
324 
325         // And now it should have started
326         verify(lifecycleAware).onStart();
327         // Check resource management in the exception case also
328         verify(bundleContext).ungetService(service);
329     }
330 
331     @Test
332     public void staleServiceReferencesReturningNullBundleAndLifecycleAreHandled() throws Exception
333     {
334         verifyStaleServiceReferencesAreHandled(true);
335     }
336 
337     @Test
338     public void staleServiceReferencesReturningNullLifecycleAreHandled() throws Exception
339     {
340         verifyStaleServiceReferencesAreHandled(false);
341     }
342 
343     private void verifyStaleServiceReferencesAreHandled(final boolean makeBundleNull) throws Exception
344     {
345         // This tests a race between DefaultLifecycleManager.notifyLifecycleAwareIfStartedAndEnabled() and service deregistration.
346         // It's not worth the effort of actually constructing the race, we can just rig return values ahead of time.
347         defaultLifecycleManager.afterPropertiesSet();
348 
349         final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
350         final ServiceReference<LifecycleAware> service = registerService(pluginBundle, lifecycleAware);
351         enablePlugin(pluginKey);
352 
353         if (makeBundleNull)
354         {
355             when(service.getBundle()).thenReturn(null);
356         }
357         when(bundleContext.getService(service)).thenReturn(null);
358 
359         isApplicationSetup = true;
360         defaultLifecycleManager.onPluginFrameworkStarted(new PluginFrameworkStartedEvent(pluginController, pluginAccessor));
361 
362         // In fact the majority of the verification here is that we don't throw, but it's worth checking a few details
363 
364         // We shouldn't have called onStart(). Hard to imagine how we could have, but i guess aggressive (broken) caching might.
365         verify(lifecycleAware, never()).onStart();
366         // We shouldn't unget in this case, because we didn't get successfully.
367         verify(bundleContext, never()).ungetService(service);
368     }
369 
370     private void enablePlugin(final String pluginKey)
371     {
372         // Mark plugin enabled in accessor ...
373         when(pluginAccessor.isPluginEnabled(pluginKey)).thenReturn(true);
374         // ... and fire the event
375         defaultLifecycleManager.onPluginEnabled(new PluginEnabledEvent(mock(Plugin.class)));
376     }
377 
378     private Bundle mockBundle(final String pluginKey)
379     {
380         final Bundle bundle = mock(Bundle.class);
381         final ImmutableMap<String, String> headers = ImmutableMap.<String, String>builder()
382                 .put(OsgiPlugin.ATLASSIAN_PLUGIN_KEY, pluginKey)
383                 .build();
384         when(bundle.getHeaders()).thenReturn(new Hashtable<String, String>(headers));
385         return bundle;
386     }
387 
388     private LifecycleAware mockLifecycleAwareAndRegisterService(final Bundle bundle)
389     {
390         final LifecycleAware lifecycleAware = mock(LifecycleAware.class);
391         registerService(bundle, lifecycleAware);
392         return lifecycleAware;
393     }
394 
395     private ServiceReference<LifecycleAware> registerService(final Bundle bundle, final LifecycleAware lifecycleAware)
396     {
397         @SuppressWarnings("unchecked")
398         // Mocking generics requires the unchecked cast
399         final ServiceReference<LifecycleAware> serviceReference = mock(ServiceReference.class);
400         when(serviceReference.getBundle()).thenReturn(bundle);
401         when(bundleContext.getService(serviceReference)).thenReturn(lifecycleAware);
402         // Only send the ServiceEvent if we've been initialized, which is mean serviceListener has been set
403         if (null != serviceListener)
404         {
405             serviceListener.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, serviceReference));
406         }
407         return serviceReference;
408     }
409 
410     private void unregisterService(final ServiceReference serviceReference)
411     {
412         // Only send the ServiceEvent if we've been initialized, which is mean serviceListener has been set
413         if (null != serviceListener)
414         {
415             serviceListener.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, serviceReference));
416         }
417         when(serviceReference.getBundle()).thenReturn(null);
418     }
419 }