1   /*
2    * $Id: AbstractMuleTestCase.java 11876 2008-05-31 19:08:25Z dfeist $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSource, Inc.  All rights reserved.  http://www.mulesource.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.tck;
12  
13  import org.mule.MuleServer;
14  import org.mule.RegistryContext;
15  import org.mule.api.MuleContext;
16  import org.mule.api.MuleEvent;
17  import org.mule.api.MuleEventContext;
18  import org.mule.api.MuleSession;
19  import org.mule.api.config.ConfigurationBuilder;
20  import org.mule.api.context.MuleContextBuilder;
21  import org.mule.api.context.MuleContextFactory;
22  import org.mule.api.endpoint.ImmutableEndpoint;
23  import org.mule.api.endpoint.InboundEndpoint;
24  import org.mule.api.endpoint.OutboundEndpoint;
25  import org.mule.api.routing.filter.Filter;
26  import org.mule.api.service.Service;
27  import org.mule.api.transformer.Transformer;
28  import org.mule.config.builders.DefaultsConfigurationBuilder;
29  import org.mule.config.builders.SimpleConfigurationBuilder;
30  import org.mule.context.DefaultMuleContextBuilder;
31  import org.mule.context.DefaultMuleContextFactory;
32  import org.mule.tck.testmodels.mule.TestConnector;
33  import org.mule.util.FileUtils;
34  import org.mule.util.MuleUrlStreamHandlerFactory;
35  import org.mule.util.StringMessageUtils;
36  import org.mule.util.StringUtils;
37  import org.mule.util.SystemUtils;
38  import org.mule.util.concurrent.Latch;
39  
40  import java.io.IOException;
41  import java.net.URI;
42  import java.net.URISyntaxException;
43  import java.net.URL;
44  import java.net.URLClassLoader;
45  import java.security.CodeSource;
46  import java.util.ArrayList;
47  import java.util.Collections;
48  import java.util.HashMap;
49  import java.util.HashSet;
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.Map;
53  import java.util.Properties;
54  import java.util.Set;
55  
56  import junit.framework.TestCase;
57  import junit.framework.TestResult;
58  
59  import edu.emory.mathcs.backport.java.util.concurrent.TimeUnit;
60  
61  import org.apache.commons.collections.IteratorUtils;
62  import org.apache.commons.collections.Predicate;
63  import org.apache.commons.logging.Log;
64  import org.apache.commons.logging.LogFactory;
65  
66  /**
67   * <code>AbstractMuleTestCase</code> is a base class for Mule testcases. This
68   * implementation provides services to test code for creating mock and test objects.
69   */
70  public abstract class AbstractMuleTestCase extends TestCase implements TestCaseWatchdogTimeoutHandler
71  {
72  
73      /**
74       * Top-level directories under <code>.mule</code> which are not deleted on each
75       * test case recycle. This is required, e.g. to play nice with transaction manager
76       * recovery service object store.
77       */
78      public static final String[] IGNORED_DOT_MULE_DIRS = new String[]{"transaction-log"};
79  
80      protected static MuleContext muleContext;
81  
82      /**
83       * This flag controls whether the text boxes will be logged when starting each test case.
84       */
85      private static final boolean verbose;
86  
87      // A Map of test case extension objects. JUnit creates a new TestCase instance for
88      // every method, so we need to record metainfo outside the test.
89      private static final Map testInfos = Collections.synchronizedMap(new HashMap());
90  
91      // A logger that should be suitable for most test cases.
92      protected final transient Log logger = LogFactory.getLog(this.getClass());
93  
94      /**
95       * Start the muleContext once it's configured (defaults to false for AbstractMuleTestCase, true for FunctionalTestCase).
96       */
97      private boolean startContext = false;
98  
99      // Should be set to a string message describing any prerequisites not met.
100     private boolean offline = "true".equalsIgnoreCase(System.getProperty("org.mule.offline"));
101 
102     // Barks if the test exceeds its time limit
103     private TestCaseWatchdog watchdog;
104 
105     static
106     {
107         String muleOpts = SystemUtils.getenv("MULE_TEST_OPTS");
108         if (StringUtils.isNotBlank(muleOpts))
109         {
110             Map parsedOpts = SystemUtils.parsePropertyDefinitions(muleOpts);
111             String optVerbose = (String) parsedOpts.get("mule.verbose");
112             verbose = Boolean.valueOf(optVerbose).booleanValue();
113         }
114         else
115         {
116             // per default, revert to the old behaviour
117             verbose = true;
118         }
119 
120         // register the custom UrlStreamHandlerFactory.
121         MuleUrlStreamHandlerFactory.installUrlStreamHandlerFactory();
122     }
123 
124     /**
125      * Convenient test message for unit testing.
126      */
127     public static final String TEST_MESSAGE = "Test Message";
128 
129     /**
130      * Default timeout for multithreaded tests (using CountDownLatch, WaitableBoolean, etc.),
131      * in milliseconds.  The higher this value, the more reliable the test will be, so it
132      * should be set high for Continuous Integration.  However, this can waste time during
133      * day-to-day development cycles, so you may want to temporarily lower it while debugging.
134      */
135     public static final long LOCK_TIMEOUT = 30000;
136 
137     /**
138      * Default timeout for waiting for responses
139      */
140     public static final int RECEIVE_TIMEOUT = 5000;
141 
142     /**
143      * Use this as a semaphore to the unit test to indicate when a callback has successfully been called.
144      */
145     protected Latch callbackCalled;
146 
147     public AbstractMuleTestCase()
148     {
149         super();
150 
151         TestInfo info = (TestInfo) testInfos.get(getClass().getName());
152         if (info == null)
153         {
154             info = this.createTestInfo();
155             testInfos.put(getClass().getName(), info);
156         }
157         this.registerTestMethod();
158     }
159 
160     protected void registerTestMethod()
161     {
162         if (this.getName() != null)
163         {
164             this.getTestInfo().incTestCount(getName());
165         }
166     }
167 
168     public void setName(String name)
169     {
170         super.setName(name);
171         registerTestMethod();
172     }
173 
174     protected TestInfo createTestInfo()
175     {
176         return new TestInfo(this);
177     }
178 
179     protected TestInfo getTestInfo()
180     {
181         return (TestInfo) testInfos.get(this.getClass().getName());
182     }
183 
184     private void clearInfo()
185     {
186         testInfos.remove(this.getClass().getName());
187     }
188 
189     public String getName()
190     {
191         if (verbose && super.getName() != null)
192         {
193             return super.getName().substring(4).replaceAll("([A-Z])", " $1").toLowerCase() + " ";
194         }
195         return super.getName();
196     }
197 
198     public void run(TestResult result)
199     {
200         if (this.isExcluded())
201         {
202             if (verbose)
203             {
204                 logger.info(this.getClass().getName() + " excluded");
205             }
206             return;
207         }
208 
209         if (this.isDisabledInThisEnvironment())
210         {
211             if (verbose)
212             {
213                 logger.info(this.getClass().getName() + " disabled");
214             }
215             return;
216         }
217 
218         super.run(result);
219     }
220 
221     /**
222      * Shamelessly copy from Spring's ConditionalTestCase so in MULE-2.0 we can extend
223      * this class from ConditionalTestCase.
224      * 
225      * Subclasses can override <code>isDisabledInThisEnvironment</code> to skip a single test.
226      */
227     public void runBare() throws Throwable
228     {
229         // getName will return the name of the method being run. Use the real JUnit implementation,
230         // this class has a different implementation
231         if (this.isDisabledInThisEnvironment(super.getName()))
232         {
233             logger.warn(this.getClass().getName() + "." + super.getName() + " disabled in this environment");
234             return;
235         }
236 
237         // Let JUnit handle execution
238         super.runBare();
239     }
240 
241     /**
242      * Subclasses can override this method to skip the execution of the entire test class.
243      *
244      * @return <code>true</code> if the test class should not be run.
245      */
246     protected boolean isDisabledInThisEnvironment()
247     {
248         return false;
249     }
250 
251     /**
252      * Indicates whether this test has been explicitly disabled through the configuration
253      * file loaded by TestInfo.
254      *
255      * @return whether the test has been explicitly disabled
256      */
257     protected boolean isExcluded()
258     {
259         return getTestInfo().isExcluded();
260     }
261 
262     /**
263      * Should this test run?
264      *
265      * @param testMethodName name of the test method
266      * @return whether the test should execute in the current envionment
267      */
268     protected boolean isDisabledInThisEnvironment(String testMethodName)
269     {
270         return false;
271     }
272 
273     public boolean isOffline(String method)
274     {
275         if (offline)
276         {
277             logger.warn(StringMessageUtils.getBoilerPlate(
278                     "Working offline cannot run test: " + method, '=', 80));
279         }
280         return offline;
281     }
282 
283     protected boolean isDisposeManagerPerSuite()
284     {
285         return getTestInfo().isDisposeManagerPerSuite();
286     }
287 
288     protected void setDisposeManagerPerSuite(boolean val)
289     {
290         getTestInfo().setDisposeManagerPerSuite(val);
291     }
292 
293     protected TestCaseWatchdog createWatchdog()
294     {
295         return new TestCaseWatchdog(10, TimeUnit.MINUTES, this);
296     }
297 
298     public void handleTimeout(long timeout, TimeUnit unit)
299     {
300         logger.fatal("Timeout of " + unit.toMillis(timeout) + "ms exceeded - exiting VM!");
301         Runtime.getRuntime().halt(1);
302     }
303 
304     protected final void setUp() throws Exception
305     {
306         // start a watchdog thread
307         watchdog = createWatchdog();
308         watchdog.start();
309 
310         if (verbose)
311         {
312             System.out.println(StringMessageUtils.getBoilerPlate("Testing: " + toString(), '=', 80));
313         }
314 
315         try
316         {
317             if (getTestInfo().getRunCount() == 0)
318             {
319                 if (getTestInfo().isDisposeManagerPerSuite())
320                 {
321                     // We dispose here jut in case
322                     disposeManager();
323                 }
324                 suitePreSetUp();
325             }
326             if (!getTestInfo().isDisposeManagerPerSuite())
327             {
328                 // We dispose here just in case
329                 disposeManager();
330             }
331 
332             muleContext = createMuleContext();
333             if (isStartContext() && null != muleContext && muleContext.isStarted() == false)
334             {
335                 muleContext.start();
336             }
337 
338             doSetUp();
339         }
340         catch (Exception e)
341         {
342             getTestInfo().incRunCount();
343             throw e;
344         }
345     }
346 
347     protected MuleContext createMuleContext() throws Exception
348     {
349         // Should we set up the manager for every method?
350         MuleContext context;
351         if (getTestInfo().isDisposeManagerPerSuite() && muleContext != null)
352         {
353             context = muleContext;
354         }
355         else
356         {
357             MuleContextFactory muleContextFactory = new DefaultMuleContextFactory();
358             List builders = new ArrayList();
359             builders.add(new SimpleConfigurationBuilder(getStartUpProperties()));
360             builders.add(getBuilder());
361             MuleContextBuilder contextBuilder = new DefaultMuleContextBuilder();
362             configureMuleContext(contextBuilder);
363             context = muleContextFactory.createMuleContext(builders, contextBuilder);
364         }
365         return context;
366     }
367 
368     /**
369      * Override this method to set properties of the MuleContextBuilder before it is
370      * used to create the MuleContext.
371      */
372     protected void configureMuleContext(MuleContextBuilder contextBuilder)
373     {
374         contextBuilder.setWorkListener(new TestingWorkListener());
375     }
376     
377     protected ConfigurationBuilder getBuilder() throws Exception
378     {
379         return new DefaultsConfigurationBuilder();
380     }
381 
382     protected String getConfigurationResources()
383     {
384         return StringUtils.EMPTY;
385     }
386 
387     protected Properties getStartUpProperties()
388     {
389         return null;
390     }
391 
392     /**
393      * Run <strong>before</strong> any testcase setup.
394      */
395     protected void suitePreSetUp() throws Exception
396     {
397         // nothing to do
398     }
399 
400     /**
401      * Run <strong>after</strong> all testcase teardowns.
402      */
403     protected void suitePostTearDown() throws Exception
404     {
405         // nothing to do
406     }
407 
408     protected final void tearDown() throws Exception
409     {
410         try
411         {
412             doTearDown();
413 
414             if (!getTestInfo().isDisposeManagerPerSuite())
415             {
416                 disposeManager();
417             }
418         }
419         finally
420         {
421             try
422             {
423                 getTestInfo().incRunCount();
424                 if (getTestInfo().getRunCount() == getTestInfo().getTestCount())
425                 {
426                     try
427                     {
428                         suitePostTearDown();
429                     }
430                     finally
431                     {
432                         clearInfo();
433                         disposeManager();
434                     }
435                 }
436             }
437             finally
438             {
439                 // remove the watchdog thread in any case
440                 watchdog.cancel();
441             }
442         }
443     }
444 
445     protected void disposeManager()
446     {
447         try
448         {
449             if (muleContext != null && !(muleContext.isDisposed() || muleContext.isDisposing()))
450             {
451                 muleContext.dispose();
452 
453                 final String workingDir = muleContext.getConfiguration().getWorkingDirectory();
454                 // do not delete TM recovery object store, everything else is good to
455                 // go
456                 FileUtils.deleteTree(FileUtils.newFile(workingDir), IGNORED_DOT_MULE_DIRS);
457             }
458             FileUtils.deleteTree(FileUtils.newFile("./ActiveMQ"));
459         }
460         finally
461         {
462             muleContext = null;
463             RegistryContext.setRegistry(null);
464             MuleServer.setMuleContext(null);
465         }
466     }
467 
468     protected void doSetUp() throws Exception
469     {
470         // template method
471     }
472 
473     protected void doTearDown() throws Exception
474     {
475         // template method
476     }
477 
478     public static InboundEndpoint getTestInboundEndpoint(String name) throws Exception
479     {
480         return MuleTestUtils.getTestInboundEndpoint(name, muleContext);
481     }
482 
483     public static OutboundEndpoint getTestOutboundEndpoint(String name) throws Exception
484     {
485         return MuleTestUtils.getTestOutboundEndpoint(name, muleContext);
486     }
487     
488     public static InboundEndpoint getTestInboundEndpoint(String name, String uri) throws Exception
489     {
490         return MuleTestUtils.getTestInboundEndpoint(name, muleContext, uri, null, null, null);
491     }
492 
493     public static OutboundEndpoint getTestOutboundEndpoint(String name, String uri) throws Exception
494     {
495         return MuleTestUtils.getTestOutboundEndpoint(name, muleContext, uri, null, null, null);
496     }
497     
498     public static InboundEndpoint getTestInboundEndpoint(String name, List transformers) throws Exception
499     {
500         return MuleTestUtils.getTestInboundEndpoint(name, muleContext, null, transformers, null, null);
501     }
502 
503     public static OutboundEndpoint getTestOutboundEndpoint(String name, List transformers) throws Exception
504     {
505         return MuleTestUtils.getTestOutboundEndpoint(name, muleContext, null, transformers, null, null);
506     }
507     
508     public static InboundEndpoint getTestInboundEndpoint(String name, String uri, List transformers, Filter filter, Map properties) throws Exception
509     {
510         return MuleTestUtils.getTestInboundEndpoint(name, muleContext, uri, transformers, filter, properties);
511     }
512 
513     public static OutboundEndpoint getTestOutboundEndpoint(String name, String uri, List transformers, Filter filter, Map properties) throws Exception
514     {
515         return MuleTestUtils.getTestOutboundEndpoint(name, muleContext, uri, transformers, filter, properties);
516     }
517 
518     public static MuleEvent getTestEvent(Object data, Service service) throws Exception
519     {
520         return MuleTestUtils.getTestEvent(data, service, muleContext);
521     }
522 
523     public static MuleEvent getTestEvent(Object data) throws Exception
524     {
525         return MuleTestUtils.getTestEvent(data, muleContext);
526     }
527     
528     public static MuleEvent getTestInboundEvent(Object data) throws Exception
529     {
530         return MuleTestUtils.getTestInboundEvent(data, muleContext);
531     }
532 
533     public static MuleEventContext getTestEventContext(Object data) throws Exception
534     {
535         return MuleTestUtils.getTestEventContext(data, muleContext);
536     }
537 
538     public static Transformer getTestTransformer() throws Exception
539     {
540         return MuleTestUtils.getTestTransformer();
541     }
542 
543     public static MuleEvent getTestEvent(Object data, ImmutableEndpoint endpoint) throws Exception
544     {
545         return MuleTestUtils.getTestEvent(data, endpoint, muleContext);
546     }
547 
548     public static MuleEvent getTestEvent(Object data, Service service, ImmutableEndpoint endpoint)
549         throws Exception
550     {
551         return MuleTestUtils.getTestEvent(data, service, endpoint, muleContext);
552     }
553 
554     public static MuleSession getTestSession(Service service, MuleContext context)
555     {
556         return MuleTestUtils.getTestSession(service, context);
557     }
558 
559     public static TestConnector getTestConnector() throws Exception
560     {
561         return MuleTestUtils.getTestConnector(muleContext);
562     }
563 
564     public static Service getTestService() throws Exception
565     {
566         return MuleTestUtils.getTestService(muleContext);
567     }
568 
569     public static Service getTestService(String name, Class clazz) throws Exception
570     {
571         return MuleTestUtils.getTestService(name, clazz, muleContext);
572     }
573 
574     public static Service getTestService(String name, Class clazz, Map props) throws Exception
575     {
576         return MuleTestUtils.getTestService(name, clazz, props, muleContext);
577     }
578 
579     public static class TestInfo
580     {
581         /**
582          * Whether to dispose the manager after every method or once all tests for
583          * the class have run
584          */
585         private final String name;
586         private boolean disposeManagerPerSuite = false;
587         private boolean excluded = false;
588         private volatile int testCount = 0;
589         private volatile int runCount = 0;
590         // @GuardedBy(this)
591         private Set registeredTestMethod = new HashSet();
592 
593         // this is a shorter version of the snippet from:
594         // http://www.davidflanagan.com/blog/2005_06.html#000060
595         // (see comments; DF's "manual" version works fine too)
596         public static URL getClassPathRoot(Class clazz)
597         {
598             CodeSource cs = clazz.getProtectionDomain().getCodeSource();
599             return (cs != null ? cs.getLocation() : null);
600         }
601 
602         public TestInfo(TestCase test)
603         {
604             this.name = test.getClass().getName();
605 
606             // load test exclusions
607             try
608             {
609                 // We find the physical classpath root URL of the test class and
610                 // use that to find the correct resource. Works fine everywhere,
611                 // regardless of classloaders. See MULE-2414
612                 URL[] urls = new URL[]{getClassPathRoot(test.getClass())};
613                 URL fileUrl = new URLClassLoader(urls).getResource("mule-test-exclusions.txt");
614 
615                 if (fileUrl != null)
616                 {
617                     // in case .txt is in jar
618                     URI fileUri = new URI(StringUtils.removeStart(fileUrl.toString(), "jar:"));
619 
620                     // this iterates over all lines in the exclusion file
621                     Iterator lines = FileUtils.lineIterator(FileUtils.newFile(fileUri));
622 
623                     // ..and this finds non-comments that match the test case name
624                     excluded = IteratorUtils.filteredIterator(lines, new Predicate()
625                     {
626                         public boolean evaluate(Object object)
627                         {
628                             return StringUtils.equals(name, StringUtils.trimToEmpty((String) object));
629                         }
630                     }).hasNext();
631                 }
632             }
633             catch (IOException ioex)
634             {
635                 // ignore
636             }
637             catch (URISyntaxException e)
638             {
639                 // ignore
640             }
641         }
642 
643         public int getTestCount()
644         {
645             return testCount;
646         }
647 
648         public synchronized void incTestCount(String name)
649         {
650             if (!registeredTestMethod.contains(name))
651             {
652                 testCount++;
653                 registeredTestMethod.add(name);
654             }
655         }
656 
657         public int getRunCount()
658         {
659             return runCount;
660         }
661 
662         public void incRunCount()
663         {
664             runCount++;
665         }
666 
667         public String getName()
668         {
669             return name;
670         }
671 
672         public boolean isDisposeManagerPerSuite()
673         {
674             return disposeManagerPerSuite;
675         }
676 
677         public void setDisposeManagerPerSuite(boolean disposeManagerPerSuite)
678         {
679             this.disposeManagerPerSuite = disposeManagerPerSuite;
680         }
681 
682         public boolean isExcluded()
683         {
684             return excluded;
685         }
686 
687         public synchronized String toString()
688         {
689             StringBuffer buf = new StringBuffer();
690             return buf.append(name).append(", (").append(runCount).append(" / ").append(testCount).append(
691                     ") tests run, disposePerSuite=").append(disposeManagerPerSuite).toString();
692         }
693     }
694 
695     protected boolean isStartContext()
696     {
697         return startContext;
698     }
699 
700     protected void setStartContext(boolean startContext)
701     {
702         this.startContext = startContext;
703     }
704 }