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