View Javadoc
1   package com.atlassian.plugin.manager;
2   
3   import com.atlassian.plugin.DefaultModuleDescriptorFactory;
4   import com.atlassian.plugin.ModuleDescriptor;
5   import com.atlassian.plugin.ModuleDescriptorFactory;
6   import com.atlassian.plugin.Plugin;
7   import com.atlassian.plugin.PluginException;
8   import com.atlassian.plugin.PluginParseException;
9   import com.atlassian.plugin.PluginRestartState;
10  import com.atlassian.plugin.PluginState;
11  import com.atlassian.plugin.SplitStartupPluginSystemLifecycle;
12  import com.atlassian.plugin.descriptors.AbstractModuleDescriptor;
13  import com.atlassian.plugin.event.PluginEventListener;
14  import com.atlassian.plugin.event.PluginEventManager;
15  import com.atlassian.plugin.event.events.PluginEnabledEvent;
16  import com.atlassian.plugin.event.events.PluginFrameworkDelayedEvent;
17  import com.atlassian.plugin.event.events.PluginFrameworkResumingEvent;
18  import com.atlassian.plugin.event.events.PluginFrameworkShutdownEvent;
19  import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
20  import com.atlassian.plugin.event.events.PluginFrameworkStartingEvent;
21  import com.atlassian.plugin.event.impl.DefaultPluginEventManager;
22  import com.atlassian.plugin.exception.PluginExceptionInterception;
23  import com.atlassian.plugin.hostcontainer.DefaultHostContainer;
24  import com.atlassian.plugin.loaders.DiscardablePluginLoader;
25  import com.atlassian.plugin.loaders.PluginLoader;
26  import com.atlassian.plugin.manager.store.DelegatingPluginPersistentStateStore;
27  import com.atlassian.plugin.manager.store.LoadOnlyPluginPersistentStateStore;
28  import com.atlassian.plugin.manager.store.MemoryPluginPersistentStateStore;
29  import com.atlassian.plugin.metadata.ClasspathFilePluginMetadata;
30  import com.atlassian.plugin.predicate.PluginPredicate;
31  import com.atlassian.plugin.test.CapturedLogging;
32  import com.google.common.collect.ImmutableList;
33  import org.junit.After;
34  import org.junit.Before;
35  import org.junit.Rule;
36  import org.junit.Test;
37  import org.junit.contrib.java.lang.system.RestoreSystemProperties;
38  import org.junit.rules.ExpectedException;
39  import org.mockito.ArgumentCaptor;
40  import org.mockito.invocation.InvocationOnMock;
41  import org.mockito.stubbing.Answer;
42  
43  import java.io.File;
44  import java.io.FileOutputStream;
45  import java.io.IOException;
46  import java.net.URISyntaxException;
47  import java.net.URL;
48  import java.util.Arrays;
49  import java.util.Collections;
50  import java.util.List;
51  
52  import static com.atlassian.plugin.manager.DefaultPluginManager.getLateStartupEnableRetryProperty;
53  import static com.atlassian.plugin.manager.DefaultPluginManager.getMinimumPluginVersionsFileProperty;
54  import static com.atlassian.plugin.manager.DefaultPluginManager.getStartupOverrideFileProperty;
55  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.FailureMode.FAIL_TO_ENABLE;
56  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.doAnswerPluginStateChangeWhen;
57  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockFailingModuleDescriptor;
58  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockPlugin;
59  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockPluginLoaderForPlugins;
60  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockPluginPersistentStateStore;
61  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockPluginWithVersion;
62  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockPluginsSortOrder;
63  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockStateChangePlugin;
64  import static com.atlassian.plugin.manager.DefaultPluginManagerMocks.mockStaticPlugin;
65  import static com.atlassian.plugin.test.CapturedLogging.didLogInfo;
66  import static com.atlassian.plugin.test.CapturedLogging.didLogWarn;
67  import static com.atlassian.plugin.test.PluginTestUtils.createTempDirectory;
68  import static com.google.common.collect.ImmutableList.copyOf;
69  import static org.apache.commons.io.FileUtils.deleteQuietly;
70  import static org.apache.commons.io.FileUtils.writeLines;
71  import static org.hamcrest.MatcherAssert.assertThat;
72  import static org.hamcrest.Matchers.contains;
73  import static org.hamcrest.Matchers.instanceOf;
74  import static org.hamcrest.Matchers.is;
75  import static org.hamcrest.Matchers.not;
76  import static org.hamcrest.Matchers.nullValue;
77  import static org.junit.Assert.assertEquals;
78  import static org.junit.Assert.assertTrue;
79  import static org.junit.Assert.fail;
80  import static org.mockito.Matchers.any;
81  import static org.mockito.Matchers.isA;
82  import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
83  import static org.mockito.Mockito.RETURNS_MOCKS;
84  import static org.mockito.Mockito.doAnswer;
85  import static org.mockito.Mockito.mock;
86  import static org.mockito.Mockito.never;
87  import static org.mockito.Mockito.reset;
88  import static org.mockito.Mockito.times;
89  import static org.mockito.Mockito.verify;
90  import static org.mockito.Mockito.when;
91  
92  public class TestDefaultPluginManagerLifecycle {
93      @Rule
94      public CapturedLogging capturedLogging = new CapturedLogging(DefaultPluginManager.class);
95  
96      @Rule
97      public ExpectedException expectedException = ExpectedException.none();
98  
99      @Rule
100     public RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties();
101 
102     private File temporaryDirectory;
103     private File startupOverrideFile;
104 
105     /**
106      * the object being tested
107      */
108     private SplitStartupPluginSystemLifecycle manager;
109 
110     private PluginEventManager pluginEventManager;
111 
112     private ModuleDescriptorFactory descriptorFactory = new DefaultModuleDescriptorFactory(new DefaultHostContainer());
113 
114     private DefaultPluginManager newDefaultPluginManager(PluginLoader... pluginLoaders) {
115         DefaultPluginManager dpm = DefaultPluginManager.newBuilder().
116                 withPluginLoaders(copyOf(pluginLoaders))
117                 .withModuleDescriptorFactory(descriptorFactory)
118                 .withPluginEventManager(pluginEventManager)
119                 .withStore(new MemoryPluginPersistentStateStore())
120                 .withVerifyRequiredPlugins(true)
121                 .build();
122         manager = dpm;
123         return dpm;
124     }
125 
126     @Before
127     public void setUp() throws Exception {
128         pluginEventManager = new DefaultPluginEventManager();
129         temporaryDirectory = createTempDirectory(TestDefaultPluginManager.class);
130         startupOverrideFile = new File(temporaryDirectory, "startupOverride.properties");
131     }
132 
133     @After
134     public void tearDown() throws Exception {
135         deleteQuietly(temporaryDirectory);
136     }
137 
138 
139     @Test
140     public void delayedPluginsCanBeDisabled() {
141         final String earlyKey = "earlyKey";
142         final String laterKey = "laterKey";
143 
144         Wrapper wrapper = new Wrapper(earlyKey, laterKey).invoke(true);
145         DefaultPluginManager defaultPluginManager = wrapper.getDefaultPluginManager();
146         Plugin laterPlugin = wrapper.getLaterPlugin();
147 
148         defaultPluginManager.earlyStartup();
149         defaultPluginManager.disablePlugin(laterKey);
150 
151         defaultPluginManager.lateStartup();
152         verify(laterPlugin, never()).enable();
153     }
154 
155     @Test
156     public void delayedPluginsCanBeEnabled() {
157         final String earlyKey = "earlyKey";
158         final String laterKey = "laterKey";
159 
160         Wrapper wrapper = new Wrapper(earlyKey, laterKey).invoke(false);
161         DefaultPluginManager defaultPluginManager = wrapper.getDefaultPluginManager();
162         Plugin laterPlugin = wrapper.getLaterPlugin();
163         defaultPluginManager.earlyStartup();
164         defaultPluginManager.enablePlugin(laterKey);
165 
166         defaultPluginManager.lateStartup();
167         verify(laterPlugin).enable();
168     }
169 
170     private class Wrapper {
171         private final String earlyKey;
172         private final String laterKey;
173         private Plugin laterPlugin;
174         private DefaultPluginManager defaultPluginManager;
175 
176         public Wrapper(String earlyKey, String laterKey) {
177             this.earlyKey = earlyKey;
178             this.laterKey = laterKey;
179         }
180 
181         public Plugin getLaterPlugin() {
182             return laterPlugin;
183         }
184 
185         public DefaultPluginManager getDefaultPluginManager() {
186             return defaultPluginManager;
187         }
188 
189         public Wrapper invoke(final boolean isLatePluginEnabledByDefault) {
190             final PluginPersistentStateStore pluginPersistentStateStore = mock(
191                     PluginPersistentStateStore.class, RETURNS_DEEP_STUBS);
192             when(pluginPersistentStateStore.load().getPluginRestartState(earlyKey)).thenReturn(PluginRestartState.NONE);
193             when(pluginPersistentStateStore.load().getPluginRestartState(laterKey)).thenReturn(PluginRestartState.NONE);
194             doAnswer(new Answer<Void>() {
195                 @Override
196                 public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
197                     final PluginPersistentState pluginState = (PluginPersistentState) invocationOnMock.getArguments()[0];
198                     when(pluginPersistentStateStore.load()).thenReturn(pluginState);
199                     return null;
200                 }
201             }).when(pluginPersistentStateStore).save(isA(PluginPersistentState.class));
202 
203             PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
204             Plugin earlyPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
205             laterPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
206             when(earlyPlugin.getKey()).thenReturn(earlyKey);
207             when(laterPlugin.getKey()).thenReturn(laterKey);
208             when(earlyPlugin.isEnabledByDefault()).thenReturn(true);
209             when(laterPlugin.isEnabledByDefault()).thenReturn(isLatePluginEnabledByDefault);
210             List<Plugin> bothPlugins = Arrays.asList(earlyPlugin, laterPlugin);
211             when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(bothPlugins);
212             List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
213 
214             ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
215 
216             PluginEventManager pluginEventManager = mock(PluginEventManager.class);
217 
218             PluginPredicate pluginPredicate = mock(PluginPredicate.class);
219             when(pluginPredicate.matches(earlyPlugin)).thenReturn(false);
220             when(pluginPredicate.matches(laterPlugin)).thenReturn(true);
221 
222             defaultPluginManager = new DefaultPluginManager(
223                     pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
224             return this;
225         }
226     }
227 
228     @Test
229     public void scanForNewPluginsNotAllowedBeforeLateStartup() {
230         final PluginPersistentStateStore pluginPersistentStateStore = mock(
231                 PluginPersistentStateStore.class, RETURNS_DEEP_STUBS);
232 
233         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
234         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(Arrays.<Plugin>asList());
235         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
236 
237         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
238 
239         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
240 
241         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
242                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager);
243 
244         defaultPluginManager.earlyStartup();
245 
246         expectedException.expect(IllegalStateException.class);
247         defaultPluginManager.scanForNewPlugins();
248     }
249 
250     @Test
251     public void scanForNewPluginsDuringLateStartup() {
252         final String pluginKey = "plugin-key";
253         final Plugin plugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
254         when(plugin.getKey()).thenReturn(pluginKey);
255         when(plugin.isEnabledByDefault()).thenReturn(true);
256         when(plugin.getPluginState()).thenReturn(PluginState.ENABLED);
257 
258         final PluginPersistentStateStore pluginPersistentStateStore = mock(
259                 PluginPersistentStateStore.class, RETURNS_DEEP_STUBS);
260         when(pluginPersistentStateStore.load().isEnabled(plugin)).thenReturn(true);
261 
262         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
263 
264         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
265         when(pluginLoader.loadAllPlugins(moduleDescriptorFactory)).thenReturn(Arrays.asList(plugin));
266         when(pluginLoader.loadFoundPlugins(moduleDescriptorFactory)).thenReturn(Arrays.<Plugin>asList());
267         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
268 
269         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
270 
271         final PluginPredicate pluginPredicate = mock(PluginPredicate.class);
272         when(pluginPredicate.matches(plugin)).thenReturn(true);
273 
274         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
275                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
276 
277         // Set up to do the equivalent of what connect is doing in ACDEV-1276, namely responding to plugin enable with
278         // a reentrant plugin installation.
279         final Answer<Object> scanForNewPlugins = new Answer<Object>() {
280             @Override
281             public Object answer(final InvocationOnMock invocation) throws Throwable {
282                 defaultPluginManager.scanForNewPlugins();
283                 return null;
284             }
285         };
286         doAnswer(scanForNewPlugins).when(pluginEventManager).broadcast(isA(PluginEnabledEvent.class));
287 
288         defaultPluginManager.earlyStartup();
289         defaultPluginManager.lateStartup();
290     }
291 
292 
293     @Test
294     public void earlyStartupDoesNotSavePluginPersistentState() {
295         // Make a family of plugins for each possible persistent state
296         final String pluginKeyPrefix = "pluginWithRestartState_";
297         final ImmutableList.Builder<Plugin> pluginListBuilder = ImmutableList.<Plugin>builder();
298         final PluginPersistentState.Builder pluginPersistentStateBuilder = PluginPersistentState.Builder.create();
299         for (final PluginRestartState pluginRestartState : PluginRestartState.values()) {
300             final String pluginKey = pluginKeyPrefix + pluginRestartState.toString();
301             final Plugin plugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
302             when(plugin.getKey()).thenReturn(pluginKey);
303             pluginListBuilder.add(plugin);
304             pluginPersistentStateBuilder.setPluginRestartState(pluginKey, pluginRestartState);
305 
306         }
307         final PluginPersistentStateStore pluginPersistentStateStore = mock(PluginPersistentStateStore.class);
308         when(pluginPersistentStateStore.load()).thenReturn(pluginPersistentStateBuilder.toState());
309         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
310         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(pluginListBuilder.build());
311         final List<PluginLoader> pluginLoaders = ImmutableList.of(pluginLoader);
312 
313         final PluginPredicate pluginPredicate = mock(PluginPredicate.class);
314         when(pluginPredicate.matches(any(Plugin.class))).thenReturn(false);
315 
316         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
317 
318         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
319 
320         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
321                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
322 
323         defaultPluginManager.earlyStartup();
324         verify(pluginPersistentStateStore, never()).save(any(PluginPersistentState.class));
325     }
326 
327     @Test
328     public void lateStartupRemovesPluginsMarkedForRemoval() {
329         final String earlyKey = "earlyKey";
330         final String laterKey = "laterKey";
331         final Plugin earlyPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
332         final Plugin laterPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
333         when(earlyPlugin.getKey()).thenReturn(earlyKey);
334         when(laterPlugin.getKey()).thenReturn(laterKey);
335 
336         // Since we're interested in the final computed state, we use a real PluginPersistentStateStore here
337         final PluginPersistentStateStore pluginPersistentStateStore = new MemoryPluginPersistentStateStore();
338         pluginPersistentStateStore.save(PluginPersistentState.Builder.create()
339                 .setEnabled(earlyPlugin, !earlyPlugin.isEnabledByDefault())
340                 .setEnabled(laterPlugin, !laterPlugin.isEnabledByDefault())
341                 .setPluginRestartState(earlyKey, PluginRestartState.REMOVE)
342                 .setPluginRestartState(laterKey, PluginRestartState.REMOVE)
343                 .toState());
344 
345         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
346         final List<Plugin> bothPlugins = Arrays.asList(earlyPlugin, laterPlugin);
347         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(bothPlugins);
348         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
349 
350         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
351 
352         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
353 
354         final PluginPredicate pluginPredicate = mock(PluginPredicate.class);
355         when(pluginPredicate.matches(earlyPlugin)).thenReturn(false);
356         when(pluginPredicate.matches(laterPlugin)).thenReturn(true);
357 
358         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
359                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
360 
361         defaultPluginManager.earlyStartup();
362         verify(pluginLoader, never()).removePlugin(any(Plugin.class));
363         // We can't verify pluginPersistentStateStore.save, because it's not a Mock
364 
365         defaultPluginManager.lateStartup();
366         verify(pluginLoader).removePlugin(earlyPlugin);
367         verify(pluginLoader).removePlugin(laterPlugin);
368 
369         final PluginPersistentState pluginPersistentState = pluginPersistentStateStore.load();
370         // Enablement should have fallen back to default state
371         assertThat(pluginPersistentState.isEnabled(earlyPlugin), is(earlyPlugin.isEnabledByDefault()));
372         assertThat(pluginPersistentState.isEnabled(laterPlugin), is(laterPlugin.isEnabledByDefault()));
373         // Restart state should have fallen back to default state
374         assertThat(pluginPersistentState.getPluginRestartState(earlyKey), is(PluginRestartState.NONE));
375         assertThat(pluginPersistentState.getPluginRestartState(laterKey), is(PluginRestartState.NONE));
376         // This final test is arguably a bit brittle, but i think it's valid given the documentation for getMap
377         assertThat(pluginPersistentState.getMap().size(), is(0));
378     }
379 
380     @Test
381     public void exampleUsingPersistentStateDelegation() {
382         final String earlyKey = "earlyKey";
383         final String laterKey = "laterKey";
384         final Plugin earlyPlugin = mock(Plugin.class, RETURNS_MOCKS);
385         final Plugin laterPlugin = mock(Plugin.class, RETURNS_MOCKS);
386         when(earlyPlugin.getKey()).thenReturn(earlyKey);
387         when(laterPlugin.getKey()).thenReturn(laterKey);
388         when(earlyPlugin.isEnabledByDefault()).thenReturn(true);
389         when(laterPlugin.isEnabledByDefault()).thenReturn(true);
390 
391         // This is an example of using a DelegatingPluginPersistentStateStore to manage persistent state
392         final boolean tenanted[] = {false};
393         final PluginPersistentStateStore warmStore = new LoadOnlyPluginPersistentStateStore();
394         final PluginPersistentStateStore tenantedStore = new MemoryPluginPersistentStateStore();
395         final PluginPersistentStateStore pluginPersistentStateStore = new DelegatingPluginPersistentStateStore() {
396             @Override
397             public PluginPersistentStateStore getDelegate() {
398                 return !tenanted[0] ? warmStore : tenantedStore;
399             }
400         };
401 
402         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
403         final List<Plugin> bothPlugins = Arrays.asList(earlyPlugin, laterPlugin);
404         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(bothPlugins);
405         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
406 
407         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
408 
409         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
410 
411         final PluginPredicate pluginPredicate = mock(PluginPredicate.class);
412         when(pluginPredicate.matches(earlyPlugin)).thenReturn(false);
413         when(pluginPredicate.matches(laterPlugin)).thenReturn(true);
414 
415         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
416                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
417 
418         defaultPluginManager.earlyStartup();
419 
420         // Become tenanted
421         tenanted[0] = true;
422 
423         defaultPluginManager.lateStartup();
424 
425         when(earlyPlugin.getPluginState()).thenReturn(PluginState.ENABLED);
426         when(laterPlugin.getPluginState()).thenReturn(PluginState.ENABLED);
427 
428         // Change some things which get persisted
429         defaultPluginManager.disablePlugin(earlyKey);
430         defaultPluginManager.disablePlugin(laterKey);
431         // And check they persist
432         final PluginPersistentState pluginPersistentState = tenantedStore.load();
433         assertThat(pluginPersistentState.isEnabled(earlyPlugin), is(false));
434         assertThat(pluginPersistentState.isEnabled(laterPlugin), is(false));
435         // Check that we actually persisted something, which is a good check the test isn't broken.
436         assertThat(pluginPersistentState.getMap().size(), is(2));
437     }
438 
439     @Test
440     public void lateStartupDoesntRetryEnableWhenNotRequested() {
441         validateLateStartupRetryEnable(false);
442     }
443 
444     @Test
445     public void lateStartupDoesRetryEnableWhenRequested() {
446         validateLateStartupRetryEnable(true);
447     }
448 
449     private void validateLateStartupRetryEnable(final boolean allowEnableRetry) {
450         // This functionality is enabled only via a system property
451         System.setProperty(getLateStartupEnableRetryProperty(), Boolean.toString(allowEnableRetry));
452         // An ordinary plugin which enables as expected when installed
453         final Plugin enablesFinePlugin = mockStateChangePlugin("enablesFine", pluginEventManager);
454         // A plugin which does not enable when enabled()d, that is, enable() to fails first time
455         final Plugin firstEnableFailsPlugin = mockPlugin("firstEnableFails");
456         doAnswerPluginStateChangeWhen(firstEnableFailsPlugin, PluginState.INSTALLED, pluginEventManager).install();
457 
458         final PluginPersistentStateStore pluginPersistentStateStore = mockPluginPersistentStateStore();
459 
460         mockPluginsSortOrder(enablesFinePlugin, firstEnableFailsPlugin);
461 
462         final PluginLoader pluginLoader = mockPluginLoaderForPlugins(enablesFinePlugin, firstEnableFailsPlugin);
463 
464         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
465                 pluginPersistentStateStore,
466                 ImmutableList.of(pluginLoader),
467                 mock(ModuleDescriptorFactory.class),
468                 pluginEventManager,
469                 mock(PluginExceptionInterception.class)
470         );
471 
472         defaultPluginManager.earlyStartup();
473 
474         verify(enablesFinePlugin).enable();
475         verify(firstEnableFailsPlugin).enable();
476 
477         // Adjust the plugin so it will enable correctly now
478         doAnswerPluginStateChangeWhen(firstEnableFailsPlugin, PluginState.ENABLED, pluginEventManager).enable();
479 
480         defaultPluginManager.lateStartup();
481 
482         // For the plugin which enabled fine, enable has not been called again
483         verify(enablesFinePlugin).enable();
484 
485         // The other enable is left to the calling test method
486 
487         if (allowEnableRetry) {
488             // Retry is enabled, so the plugin which didn't enable during earlyStartup should have had enable reattempted,
489             // so it has now been called twice
490             verify(firstEnableFailsPlugin, times(2)).enable();
491             final String pluginString = firstEnableFailsPlugin.toString();
492             assertThat(capturedLogging, didLogWarn("Failed to enable", "fallback", "lateStartup", pluginString));
493             assertThat(capturedLogging, didLogWarn("fallback enabled", "lateStartup", pluginString));
494         } else {
495             // Retry is disabled, so still only enabled once
496             verify(firstEnableFailsPlugin, times(1)).enable();
497             // Currently there is not warning in this case. We might add a warning later, but if so it should not mention
498             // "fallback" or "lateStartup": these are the strings i'm mooting as the smoking gun for this problem in the logs.
499             assertThat(capturedLogging, not(didLogWarn("fallback", "lateStartup")));
500         }
501     }
502 
503     @Test
504     public void startupOverrideFileOverridesPluginInformationAndDelayPredicate() throws Exception {
505         final PluginPersistentStateStore pluginPersistentStateStore = mockPluginPersistentStateStore();
506 
507         final String earlyOverrideNoInfoKey = "earlyOverrideNoInfoKey";
508         final String lateOverrideNoInfoKey = "lateOverrideNoInfoKey";
509         final Plugin earlyOverrideNoInfoPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
510         final Plugin lateOverrideNoInfoPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
511         when(earlyOverrideNoInfoPlugin.getKey()).thenReturn(earlyOverrideNoInfoKey);
512         when(lateOverrideNoInfoPlugin.getKey()).thenReturn(lateOverrideNoInfoKey);
513 
514         final String earlyOverrideLateInfoKey = "earlyOverrideLateInfoKey";
515         final String lateOverrideEarlyInfoKey = "lateOverrideEarlyInfoKey";
516         final Plugin earlyOverrideLateInfoPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
517         final Plugin lateOverrideEarlyInfoPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
518         when(earlyOverrideLateInfoPlugin.getKey()).thenReturn(earlyOverrideLateInfoKey);
519         when(earlyOverrideLateInfoPlugin.getPluginInformation().getStartup()).thenReturn("late");
520         when(lateOverrideEarlyInfoPlugin.getKey()).thenReturn(lateOverrideEarlyInfoKey);
521         when(lateOverrideEarlyInfoPlugin.getPluginInformation().getStartup()).thenReturn("early");
522 
523         final List<String> overrideContents = Arrays.asList(
524                 earlyOverrideNoInfoKey + "=early",
525                 lateOverrideNoInfoKey + "=late",
526                 earlyOverrideLateInfoKey + "=early",
527                 lateOverrideEarlyInfoKey + "=late"
528         );
529         writeLines(startupOverrideFile, overrideContents);
530         System.setProperty(getStartupOverrideFileProperty(), startupOverrideFile.getPath());
531 
532         // Set the plugin sort order
533         mockPluginsSortOrder(
534                 earlyOverrideLateInfoPlugin, earlyOverrideNoInfoPlugin, lateOverrideEarlyInfoPlugin, lateOverrideNoInfoPlugin);
535 
536         final List<Plugin> allPlugins = Arrays.asList(
537                 earlyOverrideNoInfoPlugin, lateOverrideNoInfoPlugin, earlyOverrideLateInfoPlugin, lateOverrideEarlyInfoPlugin);
538 
539         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
540         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(allPlugins);
541         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
542 
543         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
544 
545         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
546 
547         final PluginPredicate pluginPredicate = mock(PluginPredicate.class);
548         // This predicate is inverted when there is nothing in the information, so that the override is doing something different.
549         // It is inverted from the information version when it is present, since we know that is taking effect (from its test),
550         // and we want to ensure we're changing that!
551         when(pluginPredicate.matches(earlyOverrideNoInfoPlugin)).thenReturn(true);
552         when(pluginPredicate.matches(lateOverrideNoInfoPlugin)).thenReturn(false);
553         when(pluginPredicate.matches(earlyOverrideLateInfoPlugin)).thenReturn(false);
554         when(pluginPredicate.matches(lateOverrideEarlyInfoPlugin)).thenReturn(true);
555 
556         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
557                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
558 
559         defaultPluginManager.earlyStartup();
560         verify(earlyOverrideNoInfoPlugin).install();
561         verify(earlyOverrideLateInfoPlugin).install();
562         verify(lateOverrideNoInfoPlugin, never()).install();
563         verify(lateOverrideEarlyInfoPlugin, never()).install();
564 
565         defaultPluginManager.lateStartup();
566         verify(lateOverrideNoInfoPlugin).install();
567         verify(lateOverrideEarlyInfoPlugin).install();
568     }
569 
570     @Test
571     public void earlyLateStartupEvents() {
572         final PluginPersistentStateStore pluginPersistentStateStore = mock(
573                 PluginPersistentStateStore.class, RETURNS_DEEP_STUBS);
574 
575         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
576         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(Arrays.<Plugin>asList());
577         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
578 
579         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
580 
581         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
582 
583         final SplitStartupPluginSystemLifecycle splitStartupPluginSystemLifecycle = new DefaultPluginManager(
584                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager);
585 
586         splitStartupPluginSystemLifecycle.earlyStartup();
587         final ArgumentCaptor<Object> earlyEvents = ArgumentCaptor.forClass(Object.class);
588         verify(pluginEventManager, times(2)).broadcast(earlyEvents.capture());
589         assertThat(earlyEvents.getAllValues(), contains(
590                 instanceOf(PluginFrameworkStartingEvent.class), instanceOf(PluginFrameworkDelayedEvent.class)));
591 
592         // reset() is a bit naughty, but we want to check events are sent when expected
593         reset(pluginEventManager);
594 
595         splitStartupPluginSystemLifecycle.lateStartup();
596         final ArgumentCaptor<Object> laterEvents = ArgumentCaptor.forClass(Object.class);
597         verify(pluginEventManager, times(2)).broadcast(laterEvents.capture());
598         assertThat(laterEvents.getAllValues(), contains(
599                 instanceOf(PluginFrameworkResumingEvent.class), instanceOf(PluginFrameworkStartedEvent.class)));
600     }
601 
602     @Test
603     public void startupElementInPluginInformationOverridesDelayPredicate() {
604         final PluginPersistentStateStore pluginPersistentStateStore = mockPluginPersistentStateStore();
605 
606         final String earlyKey = "earlyKey";
607         final String laterKey = "laterKey";
608         final Plugin earlyPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
609         final Plugin laterPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
610         when(earlyPlugin.getKey()).thenReturn(earlyKey);
611         when(earlyPlugin.getPluginInformation().getStartup()).thenReturn("early");
612         when(laterPlugin.getKey()).thenReturn(laterKey);
613         when(laterPlugin.getPluginInformation().getStartup()).thenReturn("late");
614 
615         // We need to compareTo to work for plugins that participate in the same addPlugins. Without this, this test can fail for
616         // opaque reasons (the install of laterPlugin is dropped as a duplicate). While this currently only happens when the code
617         // is broken, it feels brittle enough to guard against.
618         when(earlyPlugin.compareTo(laterPlugin)).thenReturn(-1);
619         when(laterPlugin.compareTo(earlyPlugin)).thenReturn(1);
620 
621         final PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
622         final List<Plugin> bothPlugins = Arrays.asList(earlyPlugin, laterPlugin);
623         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(bothPlugins);
624         final List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
625 
626         final ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
627 
628         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
629 
630         final PluginPredicate pluginPredicate = mock(PluginPredicate.class);
631         // This predicate is inverted from what you expect, because we want to check the PluginInformation is respected
632         when(pluginPredicate.matches(earlyPlugin)).thenReturn(true);
633         when(pluginPredicate.matches(laterPlugin)).thenReturn(false);
634 
635         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
636                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
637 
638         defaultPluginManager.earlyStartup();
639         verify(earlyPlugin).install();
640         verify(laterPlugin, never()).install();
641 
642         defaultPluginManager.lateStartup();
643         verify(laterPlugin).install();
644     }
645 
646     @Test
647     public void delayedPluginsAreDelayed() {
648         final String earlyKey = "earlyKey";
649         final String laterKey = "laterKey";
650 
651         final PluginPersistentStateStore pluginPersistentStateStore = mock(
652                 PluginPersistentStateStore.class, RETURNS_DEEP_STUBS);
653         when(pluginPersistentStateStore.load().getPluginRestartState(earlyKey)).thenReturn(PluginRestartState.NONE);
654         when(pluginPersistentStateStore.load().getPluginRestartState(laterKey)).thenReturn(PluginRestartState.NONE);
655 
656         PluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
657         Plugin earlyPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
658         Plugin laterPlugin = mock(Plugin.class, RETURNS_DEEP_STUBS);
659         when(earlyPlugin.getKey()).thenReturn(earlyKey);
660         when(laterPlugin.getKey()).thenReturn(laterKey);
661         List<Plugin> bothPlugins = Arrays.asList(earlyPlugin, laterPlugin);
662         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(bothPlugins);
663         List<PluginLoader> pluginLoaders = Arrays.asList(pluginLoader);
664 
665         ModuleDescriptorFactory moduleDescriptorFactory = mock(ModuleDescriptorFactory.class);
666 
667         PluginEventManager pluginEventManager = mock(PluginEventManager.class);
668 
669         PluginPredicate pluginPredicate = mock(PluginPredicate.class);
670         when(pluginPredicate.matches(earlyPlugin)).thenReturn(false);
671         when(pluginPredicate.matches(laterPlugin)).thenReturn(true);
672 
673         final DefaultPluginManager defaultPluginManager = new DefaultPluginManager(
674                 pluginPersistentStateStore, pluginLoaders, moduleDescriptorFactory, pluginEventManager, pluginPredicate);
675 
676         defaultPluginManager.earlyStartup();
677         assertThat(defaultPluginManager.getPlugin(earlyKey), is(earlyPlugin));
678         assertThat(defaultPluginManager.getPlugin(laterKey), nullValue());
679 
680         defaultPluginManager.lateStartup();
681         assertThat(defaultPluginManager.getPlugin(earlyKey), is(earlyPlugin));
682         assertThat(defaultPluginManager.getPlugin(laterKey), is(laterPlugin));
683     }
684 
685     public static class ThingsAreWrongListener {
686         private volatile boolean called = false;
687 
688         @PluginEventListener
689         public void onFrameworkShutdown(final PluginFrameworkShutdownEvent event) {
690             called = true;
691             throw new NullPointerException("AAAH!");
692         }
693 
694         public boolean isCalled() {
695             return called;
696         }
697     }
698 
699     @Test
700     public void testShutdownHandlesException() {
701         final ThingsAreWrongListener listener = new ThingsAreWrongListener();
702         pluginEventManager.register(listener);
703 
704         manager = newDefaultPluginManager();
705         manager.init();
706         try {
707             //this should not throw an exception
708             manager.shutdown();
709         } catch (Exception e) {
710             fail("Should not have thrown an exception!");
711         }
712         assertTrue(listener.isCalled());
713     }
714 
715     @Test
716     public void testCannotInitTwice() throws PluginParseException {
717         manager = newDefaultPluginManager();
718         manager.init();
719         try {
720             manager.init();
721             fail("IllegalStateException expected");
722         } catch (final IllegalStateException expected) {
723         }
724     }
725 
726     @Test
727     public void testCannotShutdownTwice() throws PluginParseException {
728         manager = newDefaultPluginManager();
729         manager.init();
730         manager.shutdown();
731         try {
732             manager.shutdown();
733             fail("IllegalStateException expected");
734         } catch (final IllegalStateException expected) {
735         }
736     }
737 
738     private void writeToFile(String file, String line) throws IOException, URISyntaxException {
739         final String resourceName = ClasspathFilePluginMetadata.class.getPackage().getName().replace(".", "/") + "/" + file;
740         URL resource = getClass().getClassLoader().getResource(resourceName);
741 
742         FileOutputStream fout = new FileOutputStream(new File(resource.toURI()), false);
743         fout.write(line.getBytes(), 0, line.length());
744         fout.close();
745 
746     }
747 
748     @Test
749     public void testExceptionOnRequiredPluginNotEnabling() throws Exception {
750         try {
751             writeToFile("application-required-modules.txt", "foo.required:bar");
752             writeToFile("application-required-plugins.txt", "foo.required");
753 
754             final PluginLoader mockPluginLoader = mock(PluginLoader.class);
755             final ModuleDescriptor<Object> badModuleDescriptor = mockFailingModuleDescriptor("foo.required:bar", FAIL_TO_ENABLE);
756 
757             final AbstractModuleDescriptor goodModuleDescriptor = mock(AbstractModuleDescriptor.class);
758             when(goodModuleDescriptor.getKey()).thenReturn("baz");
759             when(goodModuleDescriptor.getCompleteKey()).thenReturn("foo.required:baz");
760 
761             Plugin plugin = mockStaticPlugin("foo.required", goodModuleDescriptor, badModuleDescriptor);
762 
763             when(mockPluginLoader.loadAllPlugins(any(ModuleDescriptorFactory.class))).thenReturn(Collections.singletonList(plugin));
764 
765             manager = newDefaultPluginManager(mockPluginLoader);
766             try {
767                 manager.init();
768             } catch (PluginException e) {
769                 // expected
770                 assertEquals("Unable to validate required plugins or modules", e.getMessage());
771                 return;
772             }
773             fail("A PluginException is expected when trying to initialize the plugins system with required plugins that do not load.");
774         } finally {
775             // remove references from required files
776             writeToFile("application-required-modules.txt", "");
777             writeToFile("application-required-plugins.txt", "");
778         }
779     }
780 
781     @Test
782     public void minimumPluginVersionIsEnforced() throws Exception {
783         // This test tries to install four plugins
784         // - alpha has a minimum version specified which is less than its version, and it should be installed
785         // - beta has a minimum version specified which is equal to its version, and it should be installed
786         // - gamma has a minimum version specified which is greater than its version, and it should not be installed
787         // - delta has no minimum version specified, and it should be installed
788         // - epsilon has a garbage minimum version specified, and it should be installed
789         final String alphaKey = "alpha";
790         final String betaKey = "beta";
791         final String gammaKey = "gamma";
792         final String deltaKey = "delta";
793         final String epsilonKey = "epsilon";
794         final Plugin alphaPlugin = mockPluginWithVersion(alphaKey, "1.2");
795         final Plugin betaPlugin = mockPluginWithVersion(betaKey, "2.3");
796         final Plugin gammaPlugin = mockPluginWithVersion(gammaKey, "3.4");
797         final Plugin deltaPlugin = mockPluginWithVersion(deltaKey, "5.6");
798         final Plugin epsilonPlugin = mockPluginWithVersion(epsilonKey, "7.8");
799 
800         final Plugin[] allPlugins = {alphaPlugin, betaPlugin, gammaPlugin, deltaPlugin, epsilonPlugin};
801 
802         // We need to compareTo to work for plugins that participate in the same addPlugins, so set a good global order
803         mockPluginsSortOrder(allPlugins);
804         final DiscardablePluginLoader pluginLoader = mock(DiscardablePluginLoader.class);
805         when(pluginLoader.loadAllPlugins(isA(ModuleDescriptorFactory.class))).thenReturn(ImmutableList.copyOf(allPlugins));
806         final PluginEventManager pluginEventManager = mock(PluginEventManager.class);
807 
808         manager = new DefaultPluginManager(
809                 mockPluginPersistentStateStore(),
810                 ImmutableList.<PluginLoader>of(pluginLoader),
811                 mock(ModuleDescriptorFactory.class),
812                 pluginEventManager
813         );
814 
815         final File minimumPluginVersionsFile = new File(temporaryDirectory, "mininumPluginVersions.properties");
816         final String badEpsilonVersion = "n0V3r$Ion";
817         final String largeGammaVersion = "4.5";
818         final List<String> minimumPluginVersionsContents = Arrays.asList(
819                 alphaKey + "=0.1", // less than the version of the plugin
820                 betaKey + "=2.3", // equal to the version of the plugin
821                 gammaKey + "=" + largeGammaVersion, // greater than the version of the plugin
822                 // deltaKey is not here, because it has no specified minimum version...
823                 epsilonKey + "=" + badEpsilonVersion // garbage version
824         );
825         writeLines(minimumPluginVersionsFile, minimumPluginVersionsContents);
826         System.setProperty(getMinimumPluginVersionsFileProperty(), minimumPluginVersionsFile.getPath());
827 
828         manager.init();
829         verify(alphaPlugin).install();
830         verify(betaPlugin).install();
831         verify(gammaPlugin, never()).install();
832         // Since gammaPlugin wasn't installed, it should be discarded so the loader can reclaim resources
833         verify(pluginLoader).discardPlugin(gammaPlugin);
834         verify(deltaPlugin).install();
835         verify(epsilonPlugin).install();
836 
837         assertThat(capturedLogging, didLogInfo("Unacceptable plugin", gammaPlugin.toString(), largeGammaVersion));
838         assertThat(capturedLogging, didLogWarn("Cannot compare minimum version", epsilonPlugin.toString(), badEpsilonVersion));
839     }
840 }