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 }