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