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