View Javadoc
1   package com.atlassian.plugin.impl;
2   
3   import com.atlassian.plugin.ModuleDescriptor;
4   import com.atlassian.plugin.Plugin;
5   import com.atlassian.plugin.PluginInformation;
6   import com.atlassian.plugin.PluginState;
7   import com.atlassian.plugin.util.VersionStringComparator;
8   import org.hamcrest.Description;
9   import org.hamcrest.Matcher;
10  import org.hamcrest.Matchers;
11  import org.hamcrest.TypeSafeMatcher;
12  import org.junit.Test;
13  
14  import java.io.InputStream;
15  import java.net.URL;
16  import java.util.Date;
17  import java.util.Optional;
18  
19  import static org.hamcrest.MatcherAssert.assertThat;
20  import static org.hamcrest.Matchers.comparesEqualTo;
21  import static org.hamcrest.Matchers.empty;
22  import static org.hamcrest.Matchers.emptyIterable;
23  import static org.hamcrest.Matchers.equalTo;
24  import static org.hamcrest.Matchers.equalToIgnoringWhiteSpace;
25  import static org.hamcrest.Matchers.greaterThan;
26  import static org.hamcrest.Matchers.is;
27  import static org.hamcrest.Matchers.lessThan;
28  import static org.hamcrest.Matchers.lessThanOrEqualTo;
29  import static org.hamcrest.Matchers.not;
30  import static org.hamcrest.Matchers.notNullValue;
31  import static org.hamcrest.Matchers.nullValue;
32  import static org.junit.Assert.assertEquals;
33  import static org.junit.Assert.assertFalse;
34  import static org.junit.Assert.assertTrue;
35  import static org.mockito.Mockito.mock;
36  
37  public class TestAbstractPlugin {
38      @Test
39      public void compareToSortsByKey() {
40          final Plugin alpha = createAbstractPlugin("alpha");
41          final Plugin beta = createAbstractPlugin("beta");
42  
43          // beta should be after alpha
44          assertThat(alpha, lessThan(beta));
45          assertThat(beta, greaterThan(alpha));
46      }
47  
48      @Test
49      public void compareToSortsMilestonesBeforeNumericVersions() {
50          final Plugin milestone = createAbstractPlugin("foo", "1.2.m2");
51          final Plugin numeric = createAbstractPlugin("foo", "1.2.1");
52  
53          // numeric v1.2.1 should be after milestone v1.2.m2
54          assertThat(milestone, lessThan(numeric));
55          assertThat(numeric, greaterThan(milestone));
56      }
57  
58      @Test
59      public void compareToSortsByVersion() {
60          final Plugin lateVersion = createAbstractPlugin("foo", "3.4.1");
61          final Plugin earlyVersion = createAbstractPlugin("foo", "3.1.4");
62  
63          // lateVersion v3.4.1 should be after earlyVersion v3.1.4
64          assertThat(earlyVersion, lessThan(lateVersion));
65          assertThat(lateVersion, greaterThan(earlyVersion));
66      }
67  
68      @Test
69      public void compareToEqualWhenKeyAndVersionEqual() {
70          final Plugin firstPlugin = createAbstractPlugin("foo", "3.1.4");
71          final Plugin secondPlugin = createAbstractPlugin("foo", "3.1.4");
72  
73          // Plugins are "equal" in order
74          assertThat(firstPlugin, comparesEqualTo(secondPlugin));
75          assertThat(secondPlugin, comparesEqualTo(firstPlugin));
76      }
77  
78      @Test
79      public void compareToTreatsNullPluginInformationAsVersionZero() {
80          final Plugin nullPluginInformation = createAbstractPlugin("foo");
81          nullPluginInformation.setPluginInformation(null);
82          final Plugin defaultPluginInformation = createAbstractPlugin("foo");
83  
84          // p2 has default version (== "0.0")
85          assertThat(defaultPluginInformation.getPluginInformation().getVersion(), is("0.0"));
86          // compareTo() will "clean up" nullPluginInformation to use version "0" which is considered equal to "0.0"
87          assertThat(nullPluginInformation, comparesEqualTo(defaultPluginInformation));
88          assertThat(defaultPluginInformation, comparesEqualTo(nullPluginInformation));
89      }
90  
91      @Test
92      public void compareToSortsInvalidVersionBeforeValidVersion() throws Exception {
93          final String invalidVersion = "@$%^#";
94          assertThat(invalidVersion, not(validVersion()));
95          final String validVersion = "3.2";
96          assertThat(validVersion, validVersion());
97  
98          final Plugin invalidVersionPlugin = createAbstractPlugin("foo", invalidVersion);
99          final Plugin validVersionPlugin = createAbstractPlugin("foo", validVersion);
100 
101         // The valid version should be after the invalid version
102         assertThat(invalidVersionPlugin, lessThan(validVersionPlugin));
103         assertThat(validVersionPlugin, greaterThan(invalidVersionPlugin));
104     }
105 
106     @Test
107     public void compareToEqualWhenBothVersionsInvalid() throws Exception {
108         final Plugin firstInvalidPlugin = createAbstractPlugin("foo", "@$%^#");
109         final Plugin secondInvalidPlugin = createAbstractPlugin("foo", "!!");
110 
111         assertThat(firstInvalidPlugin.getPluginInformation().getVersion(), not(validVersion()));
112         assertThat(secondInvalidPlugin.getPluginInformation().getVersion(), not(validVersion()));
113 
114         // The plugins should sort equally
115         assertThat(firstInvalidPlugin, comparesEqualTo(secondInvalidPlugin));
116         assertThat(secondInvalidPlugin, comparesEqualTo(firstInvalidPlugin));
117     }
118 
119     @Test
120     public void compareToSortsNullKeyBeforeNonNullKey() {
121         final Plugin nullKey = createAbstractPlugin();
122         final Plugin nonNullKey = createAbstractPlugin("foo");
123 
124         // null should be before "foo"
125         assertThat(nullKey.getKey(), nullValue());
126         assertThat(nullKey, lessThan(nonNullKey));
127         assertThat(nonNullKey, greaterThan(nullKey));
128     }
129 
130     @Test
131     public void compareToEqualWhenBothKeysNull() {
132         final Plugin firstNullPlugin = createAbstractPlugin();
133         final Plugin secondNullPlugin = createAbstractPlugin();
134 
135         assertThat(firstNullPlugin.getKey(), nullValue());
136         assertThat(secondNullPlugin.getKey(), nullValue());
137         assertThat(firstNullPlugin, comparesEqualTo(secondNullPlugin));
138         assertThat(secondNullPlugin, comparesEqualTo(firstNullPlugin));
139     }
140 
141     @Test
142     public void getNameDefaultsToKey() {
143         final AbstractPlugin plugin = createAbstractPlugin("foo");
144         assertThat(plugin.getName(), is("foo"));
145     }
146 
147     @Test
148     public void getNameReturnsSetName() {
149         final Plugin plugin = createAbstractPlugin("key");
150         plugin.setI18nNameKey("i18n");
151         plugin.setName("name");
152         assertThat(plugin.getName(), is("name"));
153     }
154 
155     @Test
156     public void getNameReturnsBlankIfI18nNameKeySpecified() {
157         final Plugin plugin = createAbstractPlugin("foo");
158         plugin.setI18nNameKey("i18n");
159         assertThat(plugin.getName(), equalToIgnoringWhiteSpace(""));
160     }
161 
162     @Test(timeout = 5000)
163     public void fastAsynchronousEnableIsNotLost() {
164         final boolean[] enableThreadDidSet = new boolean[1];
165         // This is approximately what OsgiPlugin does - enableInternal moves to ENABLING, starting a background thread does
166         // the actual enable. Meanwhile, the main thread polls the state and returns. We use some synchronization to ensure
167         // we hit the race that we are interested in.
168         final Plugin plugin = new ConcretePlugin() {
169             final private Thread enableThread = new Thread() {
170                 @Override
171                 public void run() {
172                     synchronized (enableThreadDidSet) {
173                         // This models what OsgiPlugin#onPluginContainerRefresh does, and then releases the main thread
174                         enableThreadDidSet[0] = compareAndSetPluginState(PluginState.ENABLING, PluginState.ENABLED);
175                         enableThreadDidSet.notify();
176                     }
177                 }
178             };
179 
180             private PluginState slowly(final PluginState state) {
181                 try {
182                     // This wait releases the monitor on enableThreadDidSet so that the background thread can progress to
183                     // do its state manipulation, and also blocks us until it notifies us post that change so that we
184                     // ensure we hit the race condition of interest.
185                     enableThreadDidSet.wait();
186                 } catch (final InterruptedException interruptedException) {
187                     throw new RuntimeException(interruptedException);
188                 }
189                 return state;
190             }
191 
192             @Override
193             protected PluginState enableInternal() {
194                 final PluginState state = PluginState.ENABLING;
195                 setPluginState(state);
196                 // Synchronize before starting the background thread so we can block it until we get to the critical
197                 // window when returning from enableInternal
198                 synchronized (enableThreadDidSet) {
199                     enableThread.start();
200                     return slowly(PluginState.PENDING);
201                 }
202             }
203         };
204         plugin.enable();
205         // If the background thread wasn't the one that performed the enable, the test is broken
206         assertThat(enableThreadDidSet[0], is(true));
207         // We should have ended up enabled
208         assertThat(plugin.getPluginState(), is(PluginState.ENABLED));
209     }
210 
211     @Test(timeout = 5000)
212     public void slowAsynchronousEnableIsNotLost() throws Exception {
213         final boolean[] enableThreadDidSet = new boolean[1];
214         // This is approximately what OsgiPlugin does - enableInternal moves to ENABLING, starting a background thread does
215         // the actual enable. Meanwhile, the main thread polls the state and returns. We use some synchronization to ensure
216         // we hit the race that we are interested in.
217         final Plugin plugin = new ConcretePlugin() {
218             final private Thread enableThread = new Thread() {
219                 @Override
220                 public void run() {
221                     synchronized (enableThreadDidSet) {
222                         // This models what OsgiPlugin#onPluginContainerRefresh does, and then releases the main thread
223                         enableThreadDidSet[0] = compareAndSetPluginState(PluginState.ENABLING, PluginState.ENABLED);
224                         enableThreadDidSet.notify();
225                     }
226                 }
227             };
228 
229             @Override
230             protected PluginState enableInternal() {
231                 final PluginState state = PluginState.ENABLING;
232                 setPluginState(state);
233                 enableThread.start();
234                 return PluginState.PENDING;
235             }
236         };
237         // Synchronize before enabling the plugin so we can block the background thread to arrange the timing we need to
238         // check the correct transition from ENABLING to ENABLED. It's fine to synchronize here IntelliJ, the enableThread
239         // field of the inner class also has a run method which synchronizes on this.
240         //noinspection SynchronizationOnLocalVariableOrMethodParameter
241         synchronized (enableThreadDidSet) {
242             plugin.enable();
243             // Until the background thread finishes executing, the plugin is still ENABLING
244             assertThat(plugin.getPluginState(), is(PluginState.ENABLING));
245             // This wait releases the monitor on enableThreadDidSet so that the background thread can progress to
246             // do its state manipulation, and also blocks us until it notifies us post that change so that we
247             // can check the compareAndSetPluginState result and the PluginState itself.
248             enableThreadDidSet.wait();
249         }
250         // If the background thread wasn't the one that performed the enable, the test is broken
251         assertThat(enableThreadDidSet[0], is(true));
252         // We should have ended up enabled
253         assertThat(plugin.getPluginState(), is(PluginState.ENABLED));
254     }
255 
256 
257     @Test
258     public void enableTimesAreInitiallyNull() {
259         final AbstractPlugin plugin = createAbstractPlugin();
260         assertThat(plugin.getDateEnabling(), nullValue());
261         assertThat(plugin.getDateEnabled(), nullValue());
262     }
263 
264     @Test
265     public void simpleEnableSetsBothEnableTimes()
266             throws InterruptedException {
267         final AbstractPlugin plugin = createAbstractPlugin();
268 
269         final Date before = ensureTimePasses();
270 
271         plugin.enable();
272         final Date enabling = plugin.getDateEnabling();
273         final Date enabled = plugin.getDateEnabled();
274 
275         final Date after = ensureTimePasses();
276 
277 
278         assertThat(enabling, notNullValue());
279         assertThat(enabled, notNullValue());
280         assertThat(before, lessThan(enabling));
281         assertThat(enabling, lessThanOrEqualTo(enabled));
282         assertThat(enabled, lessThan(after));
283     }
284 
285     @Test
286     public void slowEnableSetsEnableTimesSeparately()
287             throws InterruptedException {
288         final SlowAbstractPlugin plugin = new SlowAbstractPlugin();
289 
290         final Date before = ensureTimePasses();
291 
292         plugin.enable();
293         assertThat(plugin.getPluginState(), is(PluginState.ENABLING));
294         assertThat(plugin.getDateEnabled(), nullValue());
295         final Date enabling = plugin.getDateEnabling();
296 
297         final Date middle = ensureTimePasses();
298 
299         final boolean finishedEnable = plugin.finishEnable();
300         assertThat(finishedEnable, is(true));
301         assertThat(plugin.getPluginState(), is(PluginState.ENABLED));
302         assertThat(plugin.getDateEnabling(), equalTo(enabling));
303         final Date enabled = plugin.getDateEnabled();
304 
305         final Date after = ensureTimePasses();
306 
307         assertThat(enabling, notNullValue());
308         assertThat(enabled, notNullValue());
309         assertThat(before, lessThan(enabling));
310         assertThat(enabling, lessThan(middle));
311         assertThat(middle, lessThan(enabled));
312         assertThat(enabled, lessThan(after));
313     }
314 
315     @Test
316     public void secondEnableClearsEnablingTime()
317             throws InterruptedException {
318         final SlowAbstractPlugin plugin = new SlowAbstractPlugin();
319 
320         plugin.enable();
321 
322         ensureTimePasses();
323 
324         assertThat(plugin.getDateEnabled(), nullValue());
325         final Date firstEnabling = plugin.getDateEnabling();
326         plugin.finishEnable();
327 
328         ensureTimePasses();
329 
330         assertThat(plugin.getDateEnabling(), equalTo(firstEnabling));
331         final Date firstEnabled = plugin.getDateEnabled();
332         plugin.disable();
333 
334         ensureTimePasses();
335 
336         assertThat(plugin.getDateEnabling(), equalTo(firstEnabling));
337         assertThat(plugin.getDateEnabled(), equalTo(firstEnabled));
338         plugin.enable();
339 
340         ensureTimePasses();
341 
342         assertThat(plugin.getDateEnabled(), nullValue());
343         final Date secondEnabling = plugin.getDateEnabling();
344         plugin.finishEnable();
345 
346         ensureTimePasses();
347 
348         assertThat(plugin.getDateEnabling(), equalTo(secondEnabling));
349         final Date secondEnabled = plugin.getDateEnabled();
350 
351         assertThat(firstEnabling, notNullValue());
352         assertThat(firstEnabled, notNullValue());
353         assertThat(secondEnabling, notNullValue());
354         assertThat(secondEnabled, notNullValue());
355         assertThat(firstEnabling, lessThan(firstEnabled));
356         assertThat(firstEnabled, lessThan(secondEnabling));
357         assertThat(secondEnabling, lessThan(secondEnabled));
358     }
359 
360     @Test
361     public void dynamicModuleDescriptors() {
362         final ModuleDescriptor moduleDescriptor = mock(ModuleDescriptor.class);
363 
364         final AbstractPlugin abstractPlugin = createAbstractPlugin();
365 
366         assertThat(abstractPlugin.getModuleDescriptors(), empty());
367         assertThat(abstractPlugin.getDynamicModuleDescriptors(), emptyIterable());
368 
369         abstractPlugin.addDynamicModuleDescriptor(moduleDescriptor);
370 
371         assertThat(abstractPlugin.getModuleDescriptors(), Matchers.contains(moduleDescriptor));
372         assertThat(abstractPlugin.getDynamicModuleDescriptors(), Matchers.contains(moduleDescriptor));
373 
374         abstractPlugin.removeDynamicModuleDescriptor(moduleDescriptor);
375 
376         assertThat(abstractPlugin.getModuleDescriptors(), empty());
377         assertThat(abstractPlugin.getDynamicModuleDescriptors(), emptyIterable());
378     }
379 
380     private AbstractPlugin createAbstractPlugin(final String key, final String version) {
381         final AbstractPlugin plugin = createAbstractPlugin(key);
382         plugin.getPluginInformation().setVersion(version);
383         return plugin;
384     }
385 
386     private AbstractPlugin createAbstractPlugin(final String key) {
387         final AbstractPlugin plugin = createAbstractPlugin();
388         plugin.setKey(key);
389         return plugin;
390     }
391 
392     private AbstractPlugin createAbstractPlugin() {
393         return new ConcretePlugin();
394     }
395 
396     /**
397      * Minimal concrete subclass of {@link AbstractPlugin} for testing.
398      */
399     private static class ConcretePlugin extends AbstractPlugin {
400         public ConcretePlugin() {
401             super(null);
402         }
403 
404         @Override
405         public boolean isUninstallable() {
406             return false;
407         }
408 
409         @Override
410         public boolean isDeleteable() {
411             return false;
412         }
413 
414         @Override
415         public boolean isDynamicallyLoaded() {
416             return false;
417         }
418 
419         @Override
420         public <T> Class<T> loadClass(final String clazz, final Class<?> callingClass) throws ClassNotFoundException {
421             return null;
422         }
423 
424         @Override
425         public ClassLoader getClassLoader() {
426             return null;
427         }
428 
429         @Override
430         public URL getResource(final String path) {
431             return null;
432         }
433 
434         @Override
435         public InputStream getResourceAsStream(final String name) {
436             return null;
437         }
438     }
439 
440     /**
441      * A test fixture that mimics the OsgiPlugin usage of AbstractPlugin.
442      */
443     private class SlowAbstractPlugin extends ConcretePlugin {
444         @Override
445         public PluginState enableInternal() {
446             return PluginState.ENABLING;
447         }
448 
449         public boolean finishEnable() {
450             return compareAndSetPluginState(PluginState.ENABLING, PluginState.ENABLED);
451         }
452     }
453 
454     /**
455      * Ensure time passes during a test so timestamps can be reliably compared.
456      *
457      * @return a date which is strictly between the time before and the time after this method was
458      * called.
459      */
460     private Date ensureTimePasses()
461             throws InterruptedException {
462         final long before = System.currentTimeMillis();
463         while (before >= System.currentTimeMillis()) {
464             Thread.sleep(2);
465         }
466         final Date date = new Date();
467         while (date.getTime() >= System.currentTimeMillis()) {
468             Thread.sleep(2);
469         }
470         return date;
471     }
472 
473     private static final Matcher<String> VALID_VERSION_MATCHER = new TypeSafeMatcher<String>() {
474         @Override
475         protected boolean matchesSafely(final String version) {
476             return VersionStringComparator.isValidVersionString(version);
477         }
478 
479         @Override
480         public void describeTo(final Description description) {
481             description.appendText("valid version string");
482         }
483     };
484 
485     private Matcher<String> validVersion() {
486         return VALID_VERSION_MATCHER;
487     }
488 
489     // These methods test the plugin compareTo() function, which compares plugins based on their version numbers.
490     @Test
491     public void testComparePluginNewer() {
492 
493         final Plugin p1 = createPluginWithVersion("1.1");
494         final Plugin p2 = createPluginWithVersion("1.0");
495         assertTrue(p1.compareTo(p2) == 1);
496 
497         p1.getPluginInformation().setVersion("1.10");
498         p2.getPluginInformation().setVersion("1.2");
499         assertTrue(p1.compareTo(p2) == 1);
500 
501         p1.getPluginInformation().setVersion("1.2");
502         p2.getPluginInformation().setVersion("1.01");
503         assertTrue(p1.compareTo(p2) == 1);
504 
505         p1.getPluginInformation().setVersion("1.0.1");
506         p2.getPluginInformation().setVersion("1.0");
507         assertTrue(p1.compareTo(p2) == 1);
508 
509         p1.getPluginInformation().setVersion("1.2");
510         p2.getPluginInformation().setVersion("1.1.1");
511         assertTrue(p1.compareTo(p2) == 1);
512     }
513 
514     @Test
515     public void testComparePluginOlder() {
516         final Plugin p1 = createPluginWithVersion("1.0");
517         final Plugin p2 = createPluginWithVersion("1.1");
518         assertTrue(p1.compareTo(p2) == -1);
519 
520         p1.getPluginInformation().setVersion("1.2");
521         p2.getPluginInformation().setVersion("1.10");
522         assertTrue(p1.compareTo(p2) == -1);
523 
524         p1.getPluginInformation().setVersion("1.01");
525         p2.getPluginInformation().setVersion("1.2");
526         assertTrue(p1.compareTo(p2) == -1);
527 
528         p1.getPluginInformation().setVersion("1.0");
529         p2.getPluginInformation().setVersion("1.0.1");
530         assertTrue(p1.compareTo(p2) == -1);
531 
532         p1.getPluginInformation().setVersion("1.1.1");
533         p2.getPluginInformation().setVersion("1.2");
534         assertTrue(p1.compareTo(p2) == -1);
535     }
536 
537     @Test
538     public void testComparePluginEqual() {
539         final Plugin p1 = createPluginWithVersion("1.0");
540         final Plugin p2 = createPluginWithVersion("1.0");
541         assertTrue(p1.compareTo(p2) == 0);
542 
543         p1.getPluginInformation().setVersion("1.1.0.0");
544         p2.getPluginInformation().setVersion("1.1");
545         assertTrue(p1.compareTo(p2) == 0);
546 
547         p1.getPluginInformation().setVersion(" 1 . 1 ");
548         p2.getPluginInformation().setVersion("1.1");
549         assertTrue(p1.compareTo(p2) == 0);
550     }
551 
552     // If we can't understand the version of a plugin, then take the new one.
553     @Test
554     public void testComparePluginNoVersion() {
555         final Plugin p1 = createPluginWithVersion("1.0");
556         final Plugin p2 = createPluginWithVersion("#$%");
557         assertEquals(1, p1.compareTo(p2));
558 
559         p1.getPluginInformation().setVersion("#$%");
560         p2.getPluginInformation().setVersion("1.0");
561         assertEquals(-1, p1.compareTo(p2));
562     }
563 
564     @Test
565     public void testComparePluginBadPlugin() {
566         final Plugin p1 = createPluginWithVersion("1.0");
567         final Plugin p2 = createPluginWithVersion("1.0");
568 
569         // Compare against something with a different key
570         p2.setKey("bad.key");
571         assertTrue(p1.compareTo(p2) != 0);
572     }
573 
574     @Test
575     public void testMissingScopeInPluginInformation() {
576         final Plugin p1 = createAbstractPlugin();
577         p1.getPluginInformation().setScopeKey(Optional.<String>empty());
578 
579         assertFalse(p1.getScopeKey().isPresent());
580     }
581 
582     @Test
583     public void testScopePresentInPluginInformation() {
584         final Plugin p1 = createAbstractPlugin();
585         p1.getPluginInformation().setScopeKey(Optional.of("dark-features/confluence-team-calendars"));
586 
587         assertThat(p1.getScopeKey().get(), equalTo("dark-features/confluence-team-calendars"));
588     }
589 
590     public static Plugin createPluginWithVersion(final String version) {
591         final Plugin p = new ConcretePlugin();
592         p.setKey("test.default.plugin");
593         final PluginInformation pInfo = p.getPluginInformation();
594         pInfo.setVersion(version);
595         return p;
596     }
597 }