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