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