001    /*
002     * Copyright (c) 2007-2014 Concurrent, Inc. All Rights Reserved.
003     *
004     * Project and contact information: http://www.cascading.org/
005     *
006     * This file is part of the Cascading project.
007     *
008     * Licensed under the Apache License, Version 2.0 (the "License");
009     * you may not use this file except in compliance with the License.
010     * You may obtain a copy of the License at
011     *
012     *     http://www.apache.org/licenses/LICENSE-2.0
013     *
014     * Unless required by applicable law or agreed to in writing, software
015     * distributed under the License is distributed on an "AS IS" BASIS,
016     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017     * See the License for the specific language governing permissions and
018     * limitations under the License.
019     */
020    
021    package cascading.platform;
022    
023    import java.io.IOException;
024    import java.io.InputStream;
025    import java.lang.annotation.Retention;
026    import java.lang.annotation.RetentionPolicy;
027    import java.lang.reflect.Method;
028    import java.net.URL;
029    import java.util.ArrayList;
030    import java.util.Arrays;
031    import java.util.Collections;
032    import java.util.Enumeration;
033    import java.util.HashSet;
034    import java.util.LinkedHashSet;
035    import java.util.List;
036    import java.util.Properties;
037    import java.util.Set;
038    
039    import cascading.PlatformTestCase;
040    import junit.framework.Test;
041    import org.junit.internal.runners.JUnit38ClassRunner;
042    import org.junit.runner.Description;
043    import org.junit.runner.Runner;
044    import org.junit.runner.notification.RunNotifier;
045    import org.junit.runners.BlockJUnit4ClassRunner;
046    import org.junit.runners.ParentRunner;
047    import org.junit.runners.model.InitializationError;
048    import org.slf4j.Logger;
049    import org.slf4j.LoggerFactory;
050    
051    /**
052     * Class ParentRunner is a JUnit {@link Runner} sub-class for injecting different platform and planners
053     * into the *PlatformTest classes.
054     * <p/>
055     * It works by loading the {@code platform.classname} property from the {@code cascading/platform/platform.properties}
056     * resource. Every new platform should provide this resource.
057     * <p/>
058     * To test against a specific platform, simply make sure the above resource for the platform in question is in the
059     * test CLASSPATH. The simplest way is to add it as a dependency.
060     */
061    public class PlatformRunner extends ParentRunner<Runner>
062      {
063      public static final String PLATFORM_INCLUDES = "test.platform.includes";
064      public static final String PLATFORM_RESOURCE = "cascading/platform/platform.properties";
065      public static final String PLATFORM_CLASSNAME = "platform.classname";
066    
067      private static final Logger LOG = LoggerFactory.getLogger( PlatformRunner.class );
068    
069      private Set<String> includes = new HashSet<String>();
070      private List<Runner> runners;
071    
072      @Retention(RetentionPolicy.RUNTIME)
073      public @interface Platform
074        {
075        Class<? extends TestPlatform>[] value();
076        }
077    
078      public PlatformRunner( Class<PlatformTestCase> testClass ) throws Throwable
079        {
080        super( testClass );
081    
082        setIncludes();
083        makeRunners();
084        }
085    
086      private void setIncludes()
087        {
088        String includesString = System.getProperty( PLATFORM_INCLUDES );
089    
090        if( includesString == null || includesString.isEmpty() )
091          {
092          return;
093          }
094    
095        String[] split = includesString.split( "," );
096    
097        for( String include : split )
098          {
099          includes.add( include.trim().toLowerCase() );
100          }
101        }
102    
103      public static TestPlatform makeInstance( Class<? extends TestPlatform> type )
104        {
105        try
106          {
107          return type.newInstance();
108          }
109        catch( NoClassDefFoundError exception )
110          {
111          return null;
112          }
113        catch( InstantiationException exception )
114          {
115          throw new RuntimeException( exception );
116          }
117        catch( IllegalAccessException exception )
118          {
119          throw new RuntimeException( exception );
120          }
121        }
122    
123      @Override
124      protected List<Runner> getChildren()
125        {
126        return runners;
127        }
128    
129      private List<Runner> makeRunners() throws Throwable
130        {
131        Class<?> javaClass = getTestClass().getJavaClass();
132    
133        runners = new ArrayList<Runner>();
134    
135        // test for use of annotation
136        Set<Class<? extends TestPlatform>> classes = getPlatformClassesFromAnnotation( javaClass );
137    
138        // if no platforms declared from the annotation, test classpath
139        if( classes.isEmpty() )
140          classes = getPlatformClassesFromClasspath( javaClass.getClassLoader() );
141    
142        for( Class<? extends TestPlatform> platformClass : classes )
143          {
144          addPlatform( javaClass, platformClass );
145          }
146    
147        return runners;
148        }
149    
150      private Set<Class<? extends TestPlatform>> getPlatformClassesFromAnnotation( Class<?> javaClass ) throws Throwable
151        {
152        PlatformRunner.Platform annotation = javaClass.getAnnotation( PlatformRunner.Platform.class );
153    
154        if( annotation == null )
155          return Collections.EMPTY_SET;
156    
157        HashSet<Class<? extends TestPlatform>> classes = new LinkedHashSet<Class<? extends TestPlatform>>( Arrays.asList( annotation.value() ) );
158    
159        LOG.info( "found {} test platforms from Platform annotation", classes.size() );
160    
161        return classes;
162        }
163    
164      protected static Set<Class<? extends TestPlatform>> getPlatformClassesFromClasspath( ClassLoader classLoader ) throws IOException, ClassNotFoundException
165        {
166        Set<Class<? extends TestPlatform>> classes = new LinkedHashSet<Class<? extends TestPlatform>>();
167        Properties properties = new Properties();
168    
169        LOG.debug( "classloader: {}", classLoader );
170    
171        Enumeration<URL> urls = classLoader.getResources( PLATFORM_RESOURCE );
172    
173        while( urls.hasMoreElements() )
174          {
175          InputStream stream = urls.nextElement().openStream();
176          classes.add( (Class<? extends TestPlatform>) getPlatformClass( classLoader, properties, stream ) );
177          }
178    
179        if( classes.isEmpty() )
180          {
181          LOG.warn( "no platform tests will be run" );
182          LOG.warn( "did not find {} in the classpath, no {} instances found", PLATFORM_RESOURCE, TestPlatform.class.getCanonicalName() );
183          LOG.warn( "add cascading-local, cascading-hadoop, and/or external planner library to the test classpath" );
184          }
185        else
186          {
187          LOG.info( "found {} test platforms from classpath", classes.size() );
188          }
189    
190        return classes;
191        }
192    
193      private static Class<?> getPlatformClass( ClassLoader classLoader, Properties properties, InputStream stream ) throws IOException, ClassNotFoundException
194        {
195        if( stream == null )
196          {
197          throw new IllegalStateException( "platform provider resource not found: " + PLATFORM_RESOURCE );
198          }
199    
200        properties.load( stream );
201    
202        String classname = properties.getProperty( PLATFORM_CLASSNAME );
203    
204        if( classname == null )
205          {
206          throw new IllegalStateException( "platform provider value not found: " + PLATFORM_CLASSNAME );
207          }
208    
209        Class<?> type = classLoader.loadClass( classname );
210    
211        if( type == null )
212          {
213          throw new IllegalStateException( "platform provider class not found: " + classname );
214          }
215    
216        return type;
217        }
218    
219      private void addPlatform( final Class<?> javaClass, Class<? extends TestPlatform> type ) throws Throwable
220        {
221        final TestPlatform testPlatform = makeInstance( type );
222    
223        // test platform dependencies not installed, so skip
224        if( testPlatform == null )
225          {
226          return;
227          }
228    
229        final String platformName = testPlatform.getName();
230    
231        if( !includes.isEmpty() && !includes.contains( platformName.toLowerCase() ) )
232          {
233          LOG.info( "ignoring platform: {}", platformName );
234          return;
235          }
236    
237        LOG.info( "installing platform: {}", platformName );
238        LOG.info( "running test: {}", javaClass.getName() );
239    
240        PlatformSuite suiteAnnotation = javaClass.getAnnotation( PlatformSuite.class );
241        if( suiteAnnotation != null )
242          {
243          runners.add( makeSuiteRunner( javaClass, suiteAnnotation.method(), testPlatform ) );
244          }
245        else
246          {
247          runners.add( makeClassRunner( javaClass, testPlatform, platformName ) );
248          }
249        }
250    
251      private JUnit38ClassRunner makeSuiteRunner( Class<?> javaClass, String suiteMethod, final TestPlatform testPlatform ) throws Throwable
252        {
253        Method method = javaClass.getMethod( suiteMethod, TestPlatform.class );
254    
255        return new JUnit38ClassRunner( (Test) method.invoke( null, testPlatform ) );
256        }
257    
258      private BlockJUnit4ClassRunner makeClassRunner( final Class<?> javaClass, final TestPlatform testPlatform, final String platformName ) throws InitializationError
259        {
260        return new BlockJUnit4ClassRunner( javaClass )
261        {
262        @Override
263        protected String getName()
264          {
265          return String.format( "%s[%s]", super.getName(), platformName );
266          }
267    
268    //        @Override
269    //        protected String testName( FrameworkMethod method )
270    //          {
271    //          return String.format( "%s[%s]", super.testName( method ), platformName );
272    //          }
273    
274        @Override
275        protected Object createTest() throws Exception
276          {
277          PlatformTestCase testCase = (PlatformTestCase) super.createTest();
278    
279          testCase.installPlatform( testPlatform );
280    
281          return testCase;
282          }
283        };
284        }
285    
286      @Override
287      protected Description describeChild( Runner runner )
288        {
289        return runner.getDescription();
290        }
291    
292      @Override
293      protected void runChild( Runner runner, RunNotifier runNotifier )
294        {
295        runner.run( runNotifier );
296        }
297      }