Created
July 21, 2021 20:35
-
-
Save cthornton/a06807fff78d54018086c00d44f5b068 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.common.base.Preconditions; | |
import com.google.common.collect.ImmutableList; | |
import com.google.common.collect.ImmutableListMultimap; | |
import com.google.common.collect.ListMultimap; | |
import java.lang.reflect.Method; | |
import java.util.List; | |
import java.util.Optional; | |
import java.util.function.Function; | |
import java.util.stream.Stream; | |
import org.junit.platform.commons.util.ReflectionUtils; | |
class Pair<L,R> { | |
final L left; | |
final R right; | |
Pair(L left, R right) { | |
this.left = left; | |
this.right = right; | |
} | |
} | |
class JUnit5ReflectionUtils { | |
private JUnit5ReflectionUtils() { } | |
/** | |
* Tries parsing class names that could be nested. For example, given the properly named class: | |
* | |
* com.my.SomeClass$MyNested | |
* | |
* intellij reports it as: | |
* | |
* com.my.SomeClass.MyNested | |
* | |
* Useful for @Nested support | |
*/ | |
static Class<?> parseClassNameWithNestedSupport(String className) { | |
return tryParseClassNameWithNestedSupport(className) | |
.orElseThrow(() -> new IllegalStateException("Could not find test class " + className)); | |
} | |
static Optional<Class<?>> tryParseClassNameWithNestedSupport(String className) { | |
int guessCount = 0; | |
String guess = className; | |
do { | |
try { | |
return Optional.of(Class.forName(guess)); | |
} catch (ClassNotFoundException e) { | |
// continue guessing | |
} | |
guessCount++; | |
if (guessCount >= 10) { | |
break; | |
} | |
guess = StringUtils.replaceLast(".", "$", guess); | |
} while (guess.contains(".")); | |
return Optional.empty(); | |
} | |
static Optional<Class<?>> tryParseClass(String className) { | |
try { | |
return Optional.of(Class.forName(className)); | |
} catch (ClassNotFoundException e) { | |
return Optional.empty(); | |
} | |
} | |
static boolean isPackage(String maybePackage) { | |
return Package.getPackage(maybePackage) != null; | |
} | |
// Parses a Kotlin test filter, and attempts to get the class it belongs to, and any methods selected. | |
// | |
// Cases: | |
// | |
// A: Selecting a class: com.myorg.bazel.junit.kotlin.faketests.FakeTest | |
// B: a nested class: com.myorg.bazel.junit.kotlin.faketests.FakeTest.MyNested | |
// C: a method: com.myorg.bazel.junit.kotlin.faketests.FakeTest.testTrue | |
// | |
// For scenarios A & B, this simply re-uses tryParseClassNameWithNestedSupport() directly. | |
// For scenario C, we assume that methods can't have a . in them, so we will try assuming | |
// that "com.myorg.bazel.junit.kotlin.faketests.FakeTest" is the class name, and assume "testTrue" | |
// is the method name. | |
static Optional<Pair<Class<?>, List<String>>> parseKotlin(String filter) { | |
Optional<Pair<Class<?>, List<String>>> classPair = tryParseClassNameWithNestedSupport(filter) | |
.map(klass -> new Pair<>(klass, ImmutableList.of())); | |
if (classPair.isPresent()) { | |
return classPair; | |
} | |
String methodName = StringUtils.lastSplit(filter, "\\."); | |
String filterWithoutMethod = StringUtils.replaceLast("." + methodName, "", filter); | |
return tryParseClassNameWithNestedSupport(filterWithoutMethod).map(aKlass -> { | |
List<String> fullyQualifiedMethods = getFullyQualifiedMethods(aKlass, ImmutableList.of(methodName)); | |
return new Pair<>(aKlass, fullyQualifiedMethods); | |
}); | |
} | |
static List<String> getFullyQualifiedMethods(Class<?> klass, List<String> methodNames) { | |
ListMultimap<String, Method> methodsByName = Stream.of(klass.getDeclaredMethods()) | |
.collect(ImmutableListMultimap.toImmutableListMultimap(Method::getName, Function.identity())); | |
// In the case of a test with an argument (i.e. parameterized tests), the test filter from | |
// intellij doesn't provide us with the argument name. So given something like: | |
// | |
// com.myorg.MyClass#myParameterizedMethod | |
// | |
// where it has a boolean argument, we need to adjust the actual method sent to JUnit as: | |
// | |
// | |
// Hence why we reference the list of declared methods on the Class object | |
return methodNames.stream() | |
.flatMap(methodName -> { | |
List<Method> methods = methodsByName.get(methodName); | |
Preconditions.checkState( | |
!methods.isEmpty(), | |
"Class '%s' does not have method '%s' defined", | |
klass.getName(), methodName); | |
if (methods.size() > 1) { | |
System.err.printf( | |
"More than one test method named '%s' exists on class '%s' -- %d methods will be run in no particular order\n", | |
methodName, | |
klass.getName(), | |
methods.size()); | |
} | |
return methods.stream(); | |
}) | |
.map(method -> { | |
return ReflectionUtils.getFullyQualifiedMethodName(klass, method); | |
}) | |
.collect(ImmutableList.toImmutableList()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment