View Javadoc

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