Recently, I started to contribute and maintain Pitclipse, an Eclipse plugin for running mutation testing with PIT.
We needed a mechanism to detect whether JUnit 5 is in the classpath of a Java project (so that we could run PIT with its JUnit 5 plugin).
The first implementation of such a mechanism was rather cumbersome since it manually inspected the Java project’s classpath entries. Something like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
private static boolean isJUnit5InClasspathOf(IJavaProject project) throws JavaModelException { // FIXME Naive implementation, won't handle every case (e.g. JUnit 5 provided through a junit5.jar archive) // A better implementation may rely on JDT to scan the classpath / source files for definition / use // of JUnit 5 Test annotation // // See also https://github.com/redhat-developer/vscode-java/issues/204 for (IClasspathEntry classpathEntry : project.getRawClasspath()) { if (JUnitCore.JUNIT5_CONTAINER_PATH.equals(classpathEntry.getPath())) { return true; } } for (IClasspathEntry classpathEntry : project.getResolvedClasspath(true)) { Map<String, Object> attributes = Arrays.stream(classpathEntry.getExtraAttributes()).collect(Collectors.toMap(IClasspathAttribute::getName, IClasspathAttribute::getValue, (value1, value2) -> value1)); if (isJUnit5FromMaven(attributes)) { return true; } if (isJUnit5FromGradle(classpathEntry, attributes)) { return true; } if (pointsToJunitJupiterEngineJar(classpathEntry)) { return true; } } return false; } private static boolean isJUnit5FromMaven(Map<String, Object> attributes) { if (!attributes.containsKey("maven.pomderived") || !attributes.containsKey("maven.groupId") || !attributes.containsKey("maven.artifactId")) { return false; } return "true".equals(attributes.get("maven.pomderived")) && "org.junit.jupiter".equals(attributes.get("maven.groupId")) && "junit-jupiter-engine".equals(attributes.get("maven.artifactId")); } private static boolean isJUnit5FromGradle(IClasspathEntry classpathEntry, Map<String, Object> attributes) { if (!attributes.containsKey("gradle_use_by_scope")) { return false; } return pointsToJunitJupiterEngineJar(classpathEntry); } private static boolean pointsToJunitJupiterEngineJar(IClasspathEntry classpathEntry) { try { String[] pathElements = classpathEntry.getPath().toString().split("/"); String file = pathElements[pathElements.length - 1]; return file.startsWith("junit-jupiter-engine") && file.endsWith(".jar"); } catch (IndexOutOfBoundsException e) { // path doesn't have expected format, never mind } return false; } |
That’s rather unclear, and there are lots of paths to test.
I opened an issue for investigating an alternative implementation, https://github.com/pitest/pitclipse/issues/149, and I was thinking of something simpler, along these lines:
We could try to load some classes of the JUnit 5 engine by constructing a classloader using the classpath of the Java project.
Something like (untested):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
String[] classPathEntries = JavaRuntime.computeDefaultRuntimeClassPath(javaProject); List<URL> urlList = new ArrayList<URL>(); for (int i = 0; i < classPathEntries.length; i++) { String entry = classPathEntries[i]; IPath path = new Path(entry); URL url; try { url = path.toFile().toURI().toURL(); urlList.add(url); } catch (MalformedURLException e) { e.printStackTrace(); } } URL[] urls = (URL[]) urlList.toArray(new URL[urlList.size()]); URLClassLoader classLoader = new URLClassLoader(urls, parentClassLoader); |
That’s simpler and cleaner, but we can do better than that 🙂
JDT provides a lot of API for such things. For what we need to do, we can simply rely on IJavaProject method findType:
IType findType(String fullyQualifiedName)
Returns the first type (excluding secondary types) found following this project’s classpath with the given fully qualified name or null if none is found.
So it’s just a matter of creating a reusable method, e.g.,
1 2 3 4 |
public static boolean onClassPathOf(IJavaProject project, String fullyQualifiedName) throws CoreException { return project.findType(fullyQualifiedName) != null; } |
And we can simply call it by specifying the fully qualified name of a type that we know belongs to JUnit 5 (PR https://github.com/pitest/pitclipse/pull/199):
1 2 3 4 |
private static boolean isJUnit5InClasspathOf(IJavaProject project) throws CoreException { String junit5Class = "org.junit.jupiter.engine.Constants"; return ProjectUtils.onClassPathOf(project, junit5Class); } |
Now we don’t even have to test several cases: we simply rely on the correctness of the implementation of findType. This is expected to work on Java projects, Maven projects, Gradle projects, etc.