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  }