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