001    /*
002     * Copyright 2004-2005 Stephen McConnell
003     *
004     * Licensed  under the  Apache License,  Version 2.0  (the "License");
005     * you may not use  this file  except in  compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *   http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed  under the  License is distributed on an "AS IS" BASIS,
012     * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
013     * implied.
014     *
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package net.dpml.tools.tasks;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.io.FileInputStream;
025    import java.net.URI;
026    import java.util.Enumeration;
027    import java.util.Properties;
028    import java.util.StringTokenizer;
029    
030    import net.dpml.library.Type;
031    import net.dpml.library.Resource;
032    import net.dpml.library.info.Scope;
033    
034    import net.dpml.tools.Context;
035    
036    import net.dpml.transit.Transit;
037    import net.dpml.util.PropertyResolver;
038    
039    import org.apache.tools.ant.BuildException;
040    import org.apache.tools.ant.Project;
041    import org.apache.tools.ant.taskdefs.Exit;
042    import org.apache.tools.ant.taskdefs.optional.junit.BatchTest;
043    import org.apache.tools.ant.taskdefs.optional.junit.FormatterElement;
044    import org.apache.tools.ant.taskdefs.optional.junit.JUnitTask;
045    import org.apache.tools.ant.types.Environment;
046    import org.apache.tools.ant.types.FileSet;
047    import org.apache.tools.ant.types.Path;
048    import org.apache.tools.ant.types.Commandline;
049    
050    /**
051     * JUnit test execution.
052     *
053     * @author <a href="http://www.dpml.net">Digital Product Meta Library</a>
054     * @version 1.1.3
055     */
056    public class JUnitTestTask extends GenericTask
057    {
058       /**
059        * Constant for lookup of mx value.
060        */
061        public static final String MX_KEY = "project.test.mx";
062    
063       /**
064        * Constant test enabled key.
065        */
066        public static final String TEST_ENABLED_KEY = "project.test.enabled";
067    
068       /**
069        * Constant test src directory key.
070        */
071        public static final String TEST_SRC_KEY = "project.test.src";
072    
073       /**
074        * Constant test env directory key.
075        */
076        public static final String TEST_ENV_KEY = "project.test.env";
077    
078       /**
079        * Constant test debug key.
080        */
081        public static final String DEBUG_KEY = "project.test.debug";
082    
083       /**
084        * Constant test fork key.
085        */
086        public static final String FORK_KEY = "project.test.fork";
087    
088       /**
089        * Constant test fork mode key.
090        */
091        public static final String TEST_FORK_MODE_KEY = "project.test.fork.mode";
092    
093       /**
094        * Constant test halt-on-error key (default is false).
095        */
096        public static final String HALT_ON_ERROR_KEY = "project.test.halt-on-error";
097    
098       /**
099        * Constant test halt-on-failure key (default is false).
100        */
101        public static final String HALT_ON_FAILURE_KEY = "project.test.halt-on-failure";
102    
103       /**
104        * Constant test abort on error key - if true (the default) the build will fail
105        * if a test error occurs.
106        */
107        public static final String ABORT_ON_ERROR_KEY = "project.test.exit-on-error";
108    
109       /**
110        * Constant test abort on failure key - if true (the default) the build will fail
111        * if a test failure occurs.
112        */
113        public static final String ABORT_ON_FAILURE_KEY = "project.test.exit-on-failure";
114    
115       /**
116        * Constant cache path key.
117        */
118        public static final String CACHE_PATH_KEY = "dpml.cache";
119    
120       /**
121        * Constant work dir key.
122        */
123        public static final String WORK_DIR_KEY = "project.test.dir";
124    
125        /**
126        * the key for the include pattern for test cases
127        */
128        public static final String TEST_INCLUDES_KEY = "project.test.includes";
129    
130        /**
131        * default value
132        */
133        public static final String TEST_INCLUDES_VALUE = "**/*TestCase.java, **/*Test.java";
134    
135        /**
136        * the key for the exclude pattern for test cases
137        */
138        public static final String TEST_EXCLUDES_KEY = "project.test.excludes";
139    
140       /**
141        * default value
142        */
143        public static final String TEST_EXCLUDES_VALUE = "**/Abstract*.java, **/AllTest*.java";
144    
145       /**
146        * the key for the exclude pattern for test cases
147        */
148        public static final String VERBOSE_KEY = "project.test.verbose";
149    
150        private static final String ERROR_KEY = "project.test.error";
151        private static final String FAILURE_KEY = "project.test.failure";
152        private static final String TEST_SRC_VALUE = "test";
153        private static final String TEST_ENV_VALUE = "env";
154        private static final boolean DEBUG_VALUE = true;
155        private static final boolean FORK_VALUE = true;
156        private static final boolean HALT_ON_ERROR_VALUE = false;
157        private static final boolean HALT_ON_FAILURE_VALUE = false;
158    
159        private File m_source;
160        private String m_classPathRef;
161        private Path m_classPath;
162        
163       /**
164        * Task initialization.
165        * @exception BuildException if a build error occurs.
166        */
167        public void init() throws BuildException
168        {
169            if( !isInitialized() )
170            {
171                super.init();
172                final Project project = getProject();
173                project.setNewProperty( DEBUG_KEY, "" + DEBUG_VALUE );
174                project.setNewProperty( FORK_KEY, "" + FORK_VALUE );
175                project.setNewProperty( TEST_SRC_KEY, "" + TEST_SRC_VALUE );
176                project.setNewProperty( TEST_ENV_KEY, "" + TEST_ENV_VALUE );
177                project.setNewProperty( HALT_ON_ERROR_KEY, "" + HALT_ON_ERROR_VALUE );
178                project.setNewProperty( HALT_ON_FAILURE_KEY, "" + HALT_ON_FAILURE_VALUE );
179                getContext().getPath( Scope.TEST );
180            }
181        }
182    
183       /**
184        * Set the id of the compilation classpath.
185        * @param id the classpath reference
186        */
187        public void setClasspathRef( String id ) 
188        {
189            m_classPathRef = id;
190        }
191    
192       /**
193        * Set the classpath.
194        * @param path the classpath
195        */
196        public void setClasspath( Path path ) 
197        {
198            m_classPath = path;
199        }
200    
201       /**
202        * Set the directory containing the unit test source files.
203        * @param source the test source directory
204        */
205        public void setSrc( File source )
206        {
207            m_source = source;
208        }
209        
210        private Path getClasspath()
211        {
212            if( null != m_classPath )
213            {
214                return m_classPath;
215            }
216            else if( null != m_classPathRef )
217            {
218                return (Path) getProject().getReference( m_classPathRef );
219            }
220            else
221            {
222                final String error = 
223                  "Missing classpathref or classpath argument.";
224                throw new BuildException( error, getLocation() );
225            }
226        }
227        
228       /**
229        * Task execution.
230        * @exception BuildException if a build error occurs.
231        */
232        public void execute() throws BuildException
233        {
234            if( !isTestingEnabled() )
235            {
236                return;
237            }
238            
239            final Context context = getContext();
240            final Project project = getProject();
241            final File src = context.getTargetBuildTestDirectory();
242            if( src.exists() )
243            {
244                final File working = context.getTargetTestDirectory();
245                final Path classpath = getClasspath();
246                
247                executeUnitTests( src, classpath, working );
248                
249                if( getBooleanProperty( ABORT_ON_ERROR_KEY, true ) )
250                {
251                    final String error = project.getProperty( ERROR_KEY );
252                    if( null != error )
253                    {
254                        final String message =
255                            "One or more unit test errors occured.";
256                        fail( message );
257                    }
258                }
259                
260                if( getBooleanProperty( ABORT_ON_FAILURE_KEY, true ) )
261                {
262                    final String failure = project.getProperty( FAILURE_KEY );
263                    if( null != failure )
264                    {
265                        final String message =
266                            "One or more unit test failures occured.";
267                        fail( message );
268                    }
269                }
270            }
271        }
272        
273        private void executeUnitTests( final File src, final Path classpath, File working )
274        {
275            final Project project = getProject();
276            log( "Test classpath: " + classpath, Project.MSG_VERBOSE );
277            final FileSet fileset = createFileSet( src );
278            final JUnitTask junit = (JUnitTask) project.createTask( "junit" );
279            junit.setTaskName( getTaskName() );
280            
281            final JUnitTask.SummaryAttribute summary = getSummaryAttribute();
282            junit.setPrintsummary( summary );
283            
284            junit.setShowOutput( true );
285            junit.setTempdir( working );
286            junit.setReloading( true );
287            junit.setFiltertrace( true );
288            
289            junit.createClasspath().add( classpath );
290            
291            Context context = getContext();
292            String verbose = getVerboseArgument();
293            if( null != verbose )
294            {
295                Commandline.Argument arg = junit.createJvmarg();
296                arg.setValue( "-verbose:" + verbose );
297            }
298            
299            final File reports = getContext().getTargetReportsTestDirectory();
300            mkDir( reports );
301    
302            final BatchTest batch = junit.createBatchTest();
303            batch.addFileSet( fileset );
304            batch.setTodir( reports );
305    
306            final FormatterElement plain = newConfiguredFormatter( "plain" );
307            junit.addFormatter( plain );
308            
309            final FormatterElement xml = newConfiguredFormatter( "xml" );
310            junit.addFormatter( xml );
311            
312            final Environment.Variable work = new Environment.Variable();
313            work.setKey( WORK_DIR_KEY );
314            work.setValue( working.toString() );
315            junit.addConfiguredSysproperty( work );
316    
317            final Environment.Variable testBaseDir = new Environment.Variable();
318            testBaseDir.setKey( "project.test.dir" );
319            testBaseDir.setValue( working.toString() );
320            junit.addConfiguredSysproperty( testBaseDir );
321            
322            final Environment.Variable targetDir = new Environment.Variable();
323            targetDir.setKey( "project.target.dir" );
324            targetDir.setValue( getContext().getTargetDirectory().toString() );
325            junit.addConfiguredSysproperty( targetDir );
326    
327            final Environment.Variable deliverablesDir = new Environment.Variable();
328            deliverablesDir.setKey( "project.target.deliverables.dir" );
329            deliverablesDir.setValue( getContext().getTargetDeliverablesDirectory().toString() );
330            junit.addConfiguredSysproperty( deliverablesDir );
331    
332            final Environment.Variable basedir = new Environment.Variable();
333            basedir.setKey( "basedir" );
334            basedir.setValue( project.getBaseDir().toString() );
335            junit.addConfiguredSysproperty( basedir );
336    
337            final Environment.Variable basedir2 = new Environment.Variable();
338            basedir2.setKey( "project.basedir" );
339            basedir2.setValue( project.getBaseDir().toString() );
340            junit.addConfiguredSysproperty( basedir2 );
341    
342            final Environment.Variable cache = new Environment.Variable();
343            cache.setKey( CACHE_PATH_KEY );
344            cache.setValue( getCachePath() );
345            junit.addConfiguredSysproperty( cache );
346    
347            final File policy = new File( working, "security.policy" );
348            if( policy.exists() )
349            {
350                final Environment.Variable security = new Environment.Variable();
351                security.setKey( "java.security.policy" );
352                security.setValue( policy.toString() );
353                junit.addConfiguredSysproperty( security );
354            }
355    
356            setupTestProperties( junit, project, working );
357            
358            final File logProperties = new File( working, "logging.properties" );
359            if( logProperties.exists() )
360            {
361                final Environment.Variable log = new Environment.Variable();
362                log.setKey( "dpml.logging.config" );
363                try
364                {
365                    URI logPropertiesURI = logProperties.toURI();
366                    String logPropertiesSpec = logPropertiesURI.toString();
367                    log.setValue( logPropertiesSpec );
368                }
369                catch( Exception e )
370                {
371                    final String error = 
372                      "Unexpected file to url error."
373                      + "\nFile: " + logProperties;
374                    throw new BuildException( error, e );
375                }
376                junit.addConfiguredSysproperty( log );
377            }
378            
379            String formatter = getResource().getProperty( "java.util.logging.config.class" );
380            if( null != formatter )
381            {
382                final Environment.Variable logging = new Environment.Variable();
383                logging.setKey( "java.util.logging.config.class" );
384                if( "dpml".equals( formatter ) )
385                {
386                    logging.setValue( "net.dpml.util.ConfigurationHandler" );
387                }
388                else
389                {
390                    logging.setValue( formatter );
391                }
392                junit.addConfiguredSysproperty( logging );
393            }
394            
395            final Environment.Variable endorsed = new Environment.Variable();
396            endorsed.setKey( "java.endorsed.dirs" );
397            endorsed.setValue( new File( Transit.DPML_SYSTEM, "lib/endorsed" ).getAbsolutePath() );
398            junit.addConfiguredSysproperty( endorsed );
399            
400            configureDeliverableSysProperties( junit );
401            configureForExecution( junit );
402            
403            junit.setErrorProperty( ERROR_KEY );
404            junit.setFailureProperty( FAILURE_KEY );
405            final boolean haltOnErrorPolicy = getHaltOnErrorPolicy();
406            if( haltOnErrorPolicy )
407            {
408                junit.setHaltonerror( true );
409            }
410            final boolean haltOnFailurePolicy = getHaltOnFailurePolicy();
411            if( haltOnFailurePolicy )
412            {
413                junit.setHaltonfailure( true );
414            }
415            
416            junit.init();
417            junit.execute();
418        }
419        
420        private void setupTestProperties( JUnitTask junit, Project project, File working )
421        {
422            File base = project.getBaseDir();
423            final File properties = new File( base, "test.properties" );
424            if( properties.exists() )
425            {
426                Properties props = new Properties();
427                try
428                {
429                    props.setProperty( "project.test.dir", working.getCanonicalPath() );
430                    InputStream input = new FileInputStream( properties );
431                    props.load( input );
432                    Enumeration enumeration = props.propertyNames();
433                    props = PropertyResolver.resolve( props );
434                    while( enumeration.hasMoreElements() )
435                    {
436                        String name = (String) enumeration.nextElement();
437                        final Environment.Variable v = new Environment.Variable();
438                        v.setKey( name );
439                        String propertyValue = props.getProperty( name );
440                        String parsedValue = getContext().getResource().resolve( propertyValue );
441                        v.setValue( parsedValue );
442                        junit.addConfiguredSysproperty( v );
443                    }
444                }
445                catch( IOException ioe )
446                {
447                    final String error = 
448                      "Unexpected IO error while reading " + properties.toString();
449                    throw new BuildException( error, ioe, getLocation() );
450                }
451            }
452        }
453        
454        private void configureForExecution( JUnitTask task )
455        {
456            if( getForkProperty() )
457            {
458                task.setFork( true );
459                Project project = getProject();
460                task.setDir( project.getBaseDir() );
461                JUnitTask.ForkMode mode = getForkMode();
462                if( null == mode )
463                {
464                    log( "Executing forked test." );
465                }
466                else
467                {
468                    log( "Executing forked test with mode: '" + mode + "'." );
469                    task.setForkMode( mode );
470                }
471                String mx = getContext().getProperty( MX_KEY );
472                if( null != mx )
473                {
474                    task.setMaxmemory( mx );
475                }
476            }
477            else
478            {
479                log( "executing in local jvm" );
480                JUnitTask.ForkMode mode = new JUnitTask.ForkMode( "once" );
481                task.setForkMode( mode );
482                task.setFork( false );
483            }
484        }
485        
486        private void configureDeliverableSysProperties( JUnitTask task )
487        {
488            try
489            {
490                Context context = getContext();
491                Resource resource = context.getResource();
492                Type[] types = context.getResource().getTypes();
493                for( int i=0; i<types.length; i++ )
494                {
495                    Type type = types[i];
496                    String id = type.getID();
497                    File file = context.getTargetDeliverable( id );
498                    String path = file.getCanonicalPath();
499                    final Environment.Variable variable = new Environment.Variable();
500                    variable.setKey( "project.deliverable." + id + ".path" );
501                    variable.setValue( path );
502                    task.addConfiguredSysproperty( variable );
503                }
504            }
505            catch( IOException ioe )
506            {
507                final String error = 
508                  "Unexpected IO error while building deliverable filename properties.";
509                throw new BuildException( error, ioe, getLocation() );
510            }
511        }
512    
513        private FileSet createFileSet( File src )
514        {
515            final FileSet fileset = new FileSet();
516            fileset.setDir( src );
517            addIncludes( fileset );
518            addExcludes( fileset );
519            return fileset;
520        }
521    
522        private void addIncludes( FileSet set )
523        {
524            String pattern = getTestIncludes();
525            log( "Test includes=" + pattern, Project.MSG_VERBOSE );
526            StringTokenizer tokenizer = new StringTokenizer( pattern, ", ", false );
527            while( tokenizer.hasMoreTokens() )
528            {
529                String item = tokenizer.nextToken();
530                set.createInclude().setName( item );
531            }
532        }
533    
534        private void addExcludes( FileSet set )
535        {
536            String pattern = getTestExcludes();
537            log( "Test excludes=" + pattern, Project.MSG_VERBOSE );
538            StringTokenizer tokenizer = new StringTokenizer( pattern, ", ", false );
539            while( tokenizer.hasMoreTokens() )
540            {
541                String item = tokenizer.nextToken();
542                set.createExclude().setName( item );
543            }
544        }
545    
546        private String getTestIncludes()
547        {
548            String includes = getContext().getProperty( TEST_INCLUDES_KEY );
549            if( null != includes )
550            {
551                return includes;
552            }
553            else
554            {
555                return TEST_INCLUDES_VALUE;
556            }
557        }
558    
559        private String getTestExcludes()
560        {
561            String excludes = getContext().getProperty( TEST_EXCLUDES_KEY );
562            if( null != excludes )
563            {
564                return excludes;
565            }
566            else
567            {
568                return TEST_EXCLUDES_VALUE;
569            }
570        }
571    
572        private String getCachePath()
573        {
574            final String value = getContext().getProperty( CACHE_PATH_KEY );
575            if( null != value )
576            {
577                return value;
578            }
579            else
580            {
581                File cache = (File) getProject().getReference( "dpml.cache" );
582                return cache.toString();
583            }
584        }
585    
586        private boolean getDebugProperty()
587        {
588            return getBooleanProperty( DEBUG_KEY, DEBUG_VALUE );
589        }
590    
591        private boolean getForkProperty()
592        {
593            return getBooleanProperty( FORK_KEY, FORK_VALUE );
594        }
595    
596        private JUnitTask.ForkMode getForkMode()
597        {
598            final String value = getContext().getProperty( TEST_FORK_MODE_KEY );
599            if( null == value )
600            {
601                return null;
602            }
603            else
604            {
605                return new JUnitTask.ForkMode( value );
606            }
607        }
608    
609        private boolean getBooleanProperty( final String key, final boolean fallback )
610        {
611            final String value = getContext().getProperty( key );
612            if( null == value )
613            {
614                return fallback;
615            }
616            else
617            {
618                return Project.toBoolean( value );
619            }
620        }
621    
622        private void fail( final String message )
623        {
624            final Exit exit = (Exit) getProject().createTask( "fail" );
625            exit.setMessage( message );
626            exit.init();
627            exit.execute();
628        }
629    
630        private boolean isTestingEnabled()
631        {
632            final String enabled = getContext().getProperty( TEST_ENABLED_KEY, "true" );
633            return "true".equals( enabled );
634        }
635    
636        private JUnitTask.SummaryAttribute getSummaryAttribute()
637        {
638            final Project project = getProject();
639            final JUnitTask.SummaryAttribute summary = new JUnitTask.SummaryAttribute();
640            summary.setValue( "on" );
641            return summary;
642        }
643        
644        private boolean getHaltOnErrorPolicy()
645        {
646            return getBooleanProperty(
647                HALT_ON_ERROR_KEY, HALT_ON_ERROR_VALUE );
648        }
649        
650        private boolean getHaltOnFailurePolicy()
651        {
652            return getBooleanProperty(
653                HALT_ON_FAILURE_KEY, HALT_ON_FAILURE_VALUE );
654        }
655        
656        private String getVerboseArgument()
657        {
658            Context context = getContext();
659            return context.getProperty( "project.test.verbose" );
660        }
661        
662        private FormatterElement newConfiguredFormatter( String type )
663        {
664            final FormatterElement formatter = new FormatterElement();
665            final FormatterElement.TypeAttribute attribute = new FormatterElement.TypeAttribute();
666            attribute.setValue( type );
667            formatter.setType( attribute );
668            return formatter;
669        }
670        
671    }