Skip to content

Commit

Permalink
junit5: pass static nested classes to the launcher (#195)
Browse files Browse the repository at this point in the history
This change iterates over all the static nested classes of the test
class and adds them to LauncherDiscoveryRequestBuilder to allow running
tests within static nested classes.

While Java allows more than one level of nesting, we limit our search to
one level of nesting at the moment.

Junit4 tests annotated with `RunWith(Enclosed.class)` need to be special
cased.

Refer: gradle/gradle#4427
  • Loading branch information
gaurav-narula committed Aug 15, 2023
1 parent 0ce5d05 commit d82900b
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UncheckedIOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.platform.engine.DiscoverySelector;
import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
Expand Down Expand Up @@ -47,11 +51,28 @@ public boolean run(String testClassName) {
.addPostDiscoveryFilters(TestSharding.makeShardFilter())
.build();

DiscoverySelector classSelector = DiscoverySelectors.selectClass(testClassName);
final Class<?> testClass;
try {
testClass = Class.forName(testClassName);
} catch (ClassNotFoundException e) {
throw new RuntimeException("Failed to find testClass", e);
}

// We only allow for one level of nesting at the moment
boolean enclosed = isRunWithEnclosed(testClass);
List<DiscoverySelector> classSelectors =
enclosed
? new ArrayList<>()
: Arrays.stream(testClass.getDeclaredClasses())
.filter(clazz -> Modifier.isStatic(clazz.getModifiers()))
.map(clazz -> DiscoverySelectors.selectClass(clazz))
.collect(Collectors.toList());

classSelectors.add(DiscoverySelectors.selectClass(testClassName));

LauncherDiscoveryRequestBuilder request =
LauncherDiscoveryRequestBuilder.request()
.selectors(Collections.singletonList(classSelector))
.selectors(classSelectors)
.configurationParameter(LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME, "true")
.configurationParameter(LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME, "true");

Expand Down Expand Up @@ -98,6 +119,27 @@ public boolean run(String testClassName) {
}
}

/**
* Checks if the test class is annotation with `@RunWith(Enclosed.class)`. We deliberately avoid
* using types here to avoid polluting the classpath with junit4 deps.
*/
private boolean isRunWithEnclosed(Class<?> clazz) {
for (Annotation annotation : clazz.getAnnotations()) {
Class<? extends Annotation> type = annotation.annotationType();
if (type.getName().equals("org.junit.runner.RunWith")) {
try {
Class<?> runner = (Class<?>) type.getMethod("value").invoke(annotation, (Object[]) null);
if (runner.getName().equals("org.junit.experimental.runners.Enclosed")) {
return true;
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
return false;
}
}
}
return false;
}

private File getExitFile() {
String exitFileName = System.getenv("TEST_PREMATURE_EXIT_FILE");
if (exitFileName == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ SHARDING_TEST = [
"ShardingTest.java",
]

VINTAGE_NESTED_TEST = [
"NestedClassesVintageTest.java",
]

java_test_suite(
name = "small-tests",
size = "small",
srcs = glob(
["*.java"],
exclude = PACKAGE_NAME_TEST + PARALLEL_TEST + SHARDING_TEST,
exclude = PACKAGE_NAME_TEST + PARALLEL_TEST + SHARDING_TEST + VINTAGE_NESTED_TEST,
),
exclude_engines = ["junit-vintage"],
include_engines = ["junit-jupiter"],
Expand All @@ -36,6 +40,20 @@ java_test_suite(
] + junit5_vintage_deps("contrib_rules_jvm_tests"),
)

java_test_suite(
name = "vintage-nested-test",
size = "small",
srcs = [
"NestedClassesVintageTest.java",
],
exclude_engines = ["junit-jupiter"],
include_engines = ["junit-vintage"],
runner = "junit5",
deps = [
"//java/src/com/github/bazel_contrib/contrib_rules_jvm/junit5",
] + junit5_vintage_deps("contrib_rules_jvm_tests"),
)

# Test that we can set the package name properly. We do this by
# setting the `package` attribute deliberately rather than letting
# the value be guessed at by the runner.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.github.bazel_contrib.contrib_rules_jvm.junit5;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class NestedClassesTest {
static class First {
@Test
public void shouldBeExecuted() {
System.out.println(">>>> Executed test in NestedClassesTest$First");
}
}

@Nested
@SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC")
class Second {
@Test
public void shouldBeExecuted() {
System.out.println(">>>> Executed test in NestedClassesTest$Second");
}
}

@Test
public void shouldBeExecuted() {
System.out.println(">>>> Executed test in NestedClassesTest");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.github.bazel_contrib.contrib_rules_jvm.junit5;

import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;

@RunWith(Enclosed.class)
public class NestedClassesVintageTest {
public static class First {
@Test
public void shouldBeExecuted() {
System.out.println(">>>> Executed test in NestedClassesVintageTest$First");
}
}
}

0 comments on commit d82900b

Please sign in to comment.