001/* 002 * Copyright (c) 2007-2022 The Cascading Authors. All Rights Reserved. 003 * 004 * Project and contact information: https://cascading.wensel.net/ 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 | InstantiationException | IllegalAccessException exception ) 115 { 116 throw new RuntimeException( exception ); 117 } 118 } 119 120 @Override 121 protected List<Runner> getChildren() 122 { 123 return runners; 124 } 125 126 private List<Runner> makeRunners() throws Throwable 127 { 128 Class<?> javaClass = getTestClass().getJavaClass(); 129 130 runners = new ArrayList<Runner>(); 131 132 // test for use of annotation 133 Set<Class<? extends TestPlatform>> classes = getPlatformClassesFromAnnotation( javaClass ); 134 135 // if no platforms declared from the annotation, test classpath 136 if( classes.isEmpty() ) 137 classes = getPlatformClassesFromClasspath( javaClass.getClassLoader() ); 138 139 int count = 0; 140 Iterator<Class<? extends TestPlatform>> iterator = classes.iterator(); 141 while( iterator.hasNext() ) 142 addPlatform( javaClass, iterator.next(), count++, classes.size() ); 143 144 return runners; 145 } 146 147 private Set<Class<? extends TestPlatform>> getPlatformClassesFromAnnotation( Class<?> javaClass ) throws Throwable 148 { 149 PlatformRunner.Platform annotation = javaClass.getAnnotation( PlatformRunner.Platform.class ); 150 151 if( annotation == null ) 152 return Collections.EMPTY_SET; 153 154 HashSet<Class<? extends TestPlatform>> classes = new LinkedHashSet<Class<? extends TestPlatform>>( Arrays.asList( annotation.value() ) ); 155 156 LOG.info( "found {} test platforms from Platform annotation", classes.size() ); 157 158 return classes; 159 } 160 161 static Map<ClassLoader, Set<Class<? extends TestPlatform>>> cache = new WeakHashMap<>(); 162 163 protected synchronized static Set<Class<? extends TestPlatform>> getPlatformClassesFromClasspath( ClassLoader classLoader ) throws IOException, ClassNotFoundException 164 { 165 if( cache.containsKey( classLoader ) ) 166 return cache.get( classLoader ); 167 168 Set<Class<? extends TestPlatform>> classes = new LinkedHashSet<>(); 169 Properties properties = new Properties(); 170 171 LOG.debug( "classloader: {}", classLoader ); 172 173 Enumeration<URL> urls = classLoader.getResources( PLATFORM_RESOURCE ); 174 175 while( urls.hasMoreElements() ) 176 { 177 InputStream stream = urls.nextElement().openStream(); 178 classes.add( (Class<? extends TestPlatform>) getPlatformClass( classLoader, properties, stream ) ); 179 } 180 181 if( classes.isEmpty() ) 182 { 183 LOG.warn( "no platform tests will be run" ); 184 LOG.warn( "did not find {} in the classpath, no {} instances found", PLATFORM_RESOURCE, TestPlatform.class.getCanonicalName() ); 185 LOG.warn( "add cascading-local, cascading-hadoop, and/or external planner library to the test classpath" ); 186 } 187 else 188 { 189 LOG.info( "found {} test platforms from classpath", classes.size() ); 190 } 191 192 cache.put( classLoader, classes ); 193 return classes; 194 } 195 196 private static Class<?> getPlatformClass( ClassLoader classLoader, Properties properties, InputStream stream ) throws IOException, ClassNotFoundException 197 { 198 if( stream == null ) 199 throw new IllegalStateException( "platform provider resource not found: " + PLATFORM_RESOURCE ); 200 201 properties.load( stream ); 202 203 String classname = properties.getProperty( PLATFORM_CLASSNAME ); 204 205 if( classname == null ) 206 throw new IllegalStateException( "platform provider value not found: " + PLATFORM_CLASSNAME ); 207 208 Class<?> type = classLoader.loadClass( classname ); 209 210 if( type == null ) 211 throw new IllegalStateException( "platform provider class not found: " + classname ); 212 213 return type; 214 } 215 216 private void addPlatform( final Class<?> javaClass, Class<? extends TestPlatform> type, int ordinal, int size ) throws Throwable 217 { 218 if( javaClass.getAnnotation( Ignore.class ) != null ) // ignore this class 219 { 220 LOG.info( "ignoring test class: {}", javaClass.getCanonicalName() ); 221 return; 222 } 223 224 final TestPlatform testPlatform = makeInstance( type ); 225 226 // test platform dependencies not installed, so skip 227 if( testPlatform == null ) 228 return; 229 230 final String platformName = testPlatform.getName(); 231 232 if( !includes.isEmpty() && !includes.contains( platformName.toLowerCase() ) ) 233 { 234 LOG.info( "ignoring platform: {}", platformName ); 235 return; 236 } 237 238 LOG.info( "adding test: {}, with platform: {}", javaClass.getName(), platformName ); 239 240 PlatformSuite suiteAnnotation = javaClass.getAnnotation( PlatformSuite.class ); 241 242 if( suiteAnnotation != null ) 243 runners.add( makeSuiteRunner( javaClass, suiteAnnotation.method(), testPlatform ) ); 244 else 245 runners.add( makeClassRunner( javaClass, testPlatform, platformName, size != 1 ) ); 246 } 247 248 private JUnit38ClassRunner makeSuiteRunner( Class<?> javaClass, String suiteMethod, final TestPlatform testPlatform ) throws Throwable 249 { 250 Method method = javaClass.getMethod( suiteMethod, TestPlatform.class ); 251 252 return new JUnit38ClassRunner( (Test) method.invoke( null, testPlatform ) ); 253 } 254 255 private BlockJUnit4ClassRunner makeClassRunner( final Class<?> javaClass, final TestPlatform testPlatform, final String platformName, final boolean useName ) throws InitializationError 256 { 257 return new BlockJUnit4ClassRunner( javaClass ) 258 { 259 @Override 260 protected String getName() // the runner name 261 { 262 if( useName ) 263 return String.format( "%s[%s]", super.getName(), platformName ); 264 else 265 return super.getName(); 266 } 267 268// @Override 269// protected String testName( FrameworkMethod method ) 270// { 271// return String.format( "%s[%s]", super.testName( method ), platformName ); 272// } 273 274 @Override 275 protected Object createTest() throws Exception 276 { 277 PlatformTestCase testCase = (PlatformTestCase) super.createTest(); 278 279 testCase.installPlatform( testPlatform ); 280 281 return testCase; 282 } 283 }; 284 } 285 286 @Override 287 protected Description describeChild( Runner runner ) 288 { 289 return runner.getDescription(); 290 } 291 292 @Override 293 protected void runChild( Runner runner, RunNotifier runNotifier ) 294 { 295 runner.run( runNotifier ); 296 } 297 298 @Override 299 public void filter( Filter filter ) throws NoTestsRemainException 300 { 301 for( Runner runner : getChildren() ) 302 { 303 if( runner instanceof Filterable ) 304 ( (Filterable) runner ).filter( filter ); 305 } 306 } 307 }