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 }