diff --git a/manual/Tasks/junit.html b/manual/Tasks/junit.html index 76df9ced6..9f6051217 100644 --- a/manual/Tasks/junit.html +++ b/manual/Tasks/junit.html @@ -371,6 +371,21 @@ subelement.

since Ant 1.6.

+

modulepath

+ +

The location of modules can be specified using this PATH like structure.
+The modulepath requires fork to be set to true. + +

since Ant 1.10

+ +

upgrademodulepath

+ +

The location of modules that replace upgradeable modules in the runtime image +can be specified using this PATH like structure.
+The upgrademodulepath requires fork to be set to true. + +

since Ant 1.10

+

formatter

The results of the tests can be printed in different @@ -796,7 +811,47 @@ the single <test/> will run. So only the failing test cases a The two nested formatters are for displaying (for the user) and for updating the collector class.

- - +
+    <junit fork="true"
+        jvm="${platform.java}">
+        <jvmarg value="-Xpatch:${module.name}=${build.test.classes}"/>
+        <jvmarg line="-addmods ${module.name}"/>
+        <jvmarg value="-XaddReads:${module.name}=ALL-UNNAMED"/>
+        <jvmarg value="-XaddExports:${module.name}/my.test=ALL-UNNAMED"/>
+        <classpath>
+            <pathelement path="${libs.junit}"/>
+        </classpath>
+        <modulepath>
+            <pathelement path="${modules}:${build.classes}"/>
+        </modulepath>
+        <formatter type="plain"/>
+        <test name="my.test.TestCase"/>
+    </junit>
+
+

Runs my.test.TestCase as a white-box test in the forked VM given by the platform.java property. +The junit library is a part of an unnamed module while the tested project and required modules are on the module path. The tests +do not have module-info file and are executed in the project module given by module.name property.
+The -Xpatch java option executes the tests built into ${build.test.classes} in a module given +by module.name property.
+The -addmods java option enables the tested module.
+The -XaddReads java option makes the unnamed module containing the junit readable by tested module.
+The -XaddExports java option makes the non-exported test package my.test accessible from the unnamed module containing the junit.
+

+    <junit fork="true"
+        jvm="${platform.java}">
+        <jvmarg line="-addmods ${test.module.name}"/>
+        <jvmarg value="-XaddExports:${test.module.name}/my.test=junit,ALL-UNNAMED"/>
+        <modulepath>
+            <pathelement path="${modules}:${build.classes}:${libs.junit}"/>
+        </modulepath>
+        <formatter type="plain"/>
+        <test name="my.test.TestCase"/>
+    </junit>
+
+

Runs my.test.TestCase as a black-box test in the forked VM given by the platform.java property. +The junit library is used as an automatic module. The tests module-info requires the tested module and junit.
+The -addmods java option enables the test module.
+The -XaddExports java option makes the non-exported test package my.test accessible from the junit module and Ant's test runner. +Another possibility is to export the test package in the tests module-info by exports my.test directive.
diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java index 459bd3d5d..08ae61886 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java @@ -38,6 +38,7 @@ import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.Vector; @@ -510,6 +511,26 @@ public class JUnitTask extends Task { return getCommandline().createBootclasspath(getProject()).createPath(); } + /** + * Add a path to the modulepath. + * + * @return created modulepath. + * @since 1.10 + */ + public Path createModulepath() { + return getCommandline().createModulepath(getProject()).createPath(); + } + + /** + * Add a path to the upgrademodulepath. + * + * @return created upgrademodulepath. + * @since 1.10 + */ + public Path createUpgrademodulepath() { + return getCommandline().createUpgrademodulepath(getProject()).createPath(); + } + /** * Adds an environment variable; used when forking. * @@ -749,7 +770,7 @@ public class JUnitTask extends Task { loader.loadClass("junit.framework.Test"); // sanity check } catch (final ClassNotFoundException e) { throw new BuildException( - "The for must include junit.jar " + "The or for must include junit.jar " + "if not in Ant's own classpath", e, task.getLocation()); } @@ -777,10 +798,14 @@ public class JUnitTask extends Task { if (splitJUnit) { final Path path = new Path(getProject()); path.add(antRuntimeClasses); - final Path extra = getCommandline().getClasspath(); + Path extra = getCommandline().getClasspath(); if (extra != null) { path.add(extra); } + extra = getCommandline().getModulepath(); + if (extra != null && !hasJunit(path)) { + path.add(expandModulePath(extra)); + } mirrorLoader = (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return new SplitClassLoader(myLoader, path, getProject(), @@ -818,7 +843,7 @@ public class JUnitTask extends Task { @Override public void execute() throws BuildException { checkMethodLists(); - + checkModules(); setupJUnitDelegate(); final List testLists = new ArrayList(); @@ -1697,6 +1722,75 @@ public class JUnitTask extends Task { } } + /** + * Checks a validity of module specific options. + * @since 1.10 + */ + private void checkModules() { + if (hasPath(getCommandline().getModulepath()) || + hasPath(getCommandline().getUpgrademodulepath())) { + for (int i = 0, count = batchTests.size(); i < count; i++) { + if(!batchTests.elementAt(i).getFork()) { + throw new BuildException("The module path requires fork attribute to be set to true."); + } + } + for (int i = 0, count = tests.size(); i < count; i++) { + if (!tests.elementAt(i).getFork()) { + throw new BuildException("The module path requires fork attribute to be set to true."); + } + } + } + } + + /** + * Checks is a junit is on given path. + * @param path the {@link Path} to check + * @return true when given {@link Path} contains junit + * @since 1.10 + */ + private boolean hasJunit(final Path path) { + try (AntClassLoader loader = AntClassLoader.newAntClassLoader( + null, + getProject(), + path, + true)) { + try { + loader.loadClass("junit.framework.Test"); + return true; + } catch (final Exception ex) { + return false; + } + } + } + + /** + * Expands a module path to flat path of jars and root folders usable by classloader. + * @param modulePath to be expanded + * @return the expanded path + * @since 1.10 + */ + private Path expandModulePath(Path modulePath) { + final Path expanded = new Path(getProject()); + for (String path : modulePath.list()) { + final File modulePathEntry = getProject().resolveFile(path); + if (modulePathEntry.isDirectory() && !hasModuleInfo(modulePathEntry)) { + final File[] modules = modulePathEntry.listFiles((dir,name)->name.toLowerCase(Locale.ENGLISH).endsWith(".jar")); + if (modules != null) { + for (File module : modules) { + expanded.add(new Path(getProject(), String.format( + "%s%s%s", //NOI18N + path, + File.separator, + module.getName()))); + } + } + } else { + expanded.add(new Path(getProject(), path)); + } + } + return expanded; + } + /** * return an enumeration listing each test, then each batchtest * @return enumeration @@ -1892,16 +1986,23 @@ public class JUnitTask extends Task { */ private void createClassLoader() { final Path userClasspath = getCommandline().getClasspath(); - if (userClasspath != null) { + final Path userModulepath = getCommandline().getModulepath(); + if (userClasspath != null || userModulepath != null) { if (reloading || classLoader == null) { deleteClassLoader(); - final Path classpath = (Path) userClasspath.clone(); + final Path path = new Path(getProject()); + if (userClasspath != null) { + path.add((Path) userClasspath.clone()); + } + if (userModulepath != null && !hasJunit(path)) { + path.add(expandModulePath(userModulepath)); + } if (includeAntRuntime) { log("Implicitly adding " + antRuntimeClasses + " to CLASSPATH", Project.MSG_VERBOSE); - classpath.append(antRuntimeClasses); + path.append(antRuntimeClasses); } - classLoader = getProject().createClassLoader(classpath); + classLoader = getProject().createClassLoader(path); if (getClass().getClassLoader() != null && getClass().getClassLoader() != Project.class.getClassLoader()) { classLoader.setParent(getClass().getClassLoader()); @@ -2280,4 +2381,24 @@ public class JUnitTask extends Task { w.newLine(); s.println(text); } + + /** + * Checks if a path exists and is non empty. + * @param path to be checked + * @return true if the path is non null and non empty. + * @since 1.10 + */ + private static boolean hasPath(final Path path) { + return path != null && path.size() > 0; + } + + /** + * Checks if a given folder is an unpacked module. + * @param root the fodler to be checked + * @return true if the root is an unpacked module + * @since 1.10 + */ + private static boolean hasModuleInfo(final File root) { + return new File(root, "module-info.class").exists(); //NOI18N + } } diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskTest.java index 33fd3b053..f08860aaf 100644 --- a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskTest.java +++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskTest.java @@ -27,18 +27,33 @@ import static org.apache.tools.ant.AntAssert.assertNotContains; import static org.apache.tools.ant.AntAssert.assertContains; import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathFactory; +import org.apache.tools.ant.BuildException; import org.apache.tools.ant.BuildFileRule; +import org.apache.tools.ant.MagicNames; +import org.apache.tools.ant.Project; +import org.apache.tools.ant.taskdefs.launcher.CommandLauncher; +import org.apache.tools.ant.taskdefs.optional.junit.JUnitTask.ForkMode; +import org.apache.tools.ant.types.Path; import org.apache.tools.ant.util.JavaEnvUtils; +import org.apache.tools.ant.util.LoaderUtils; import org.junit.Assume; import org.junit.Before; import org.junit.Rule; @@ -395,4 +410,193 @@ public class JUnitTaskTest { } + @Test(expected = BuildException.class) + public void testModulePathNeedsFork() throws Exception { + final Project project = new Project(); + project.init(); + JUnitTask task = new JUnitTask(); + task.setProject(project); + final Path p = new Path(project); + p.setPath("modules"); + task.createModulepath().add(p); + task.addTest(new JUnitTest("org.apache.tools.ant.taskdefs.optional.junit.TestTest")); + task.execute(); + } + + @Test(expected = BuildException.class) + public void testUpgradeModulePathNeedsFork() throws Exception { + final Project project = new Project(); + project.init(); + JUnitTask task = new JUnitTask(); + task.setProject(project); + final Path p = new Path(project); + p.setPath("modules"); + task.createUpgrademodulepath().add(p); + task.addTest(new JUnitTest("org.apache.tools.ant.taskdefs.optional.junit.TestTest")); + task.execute(); + } + + @Test + public void testJunitOnCpArguments() throws Exception { + final File tmp = new File(System.getProperty("java.io.tmpdir")); //NOI18N + final File workDir = new File(tmp, String.format("%s_testJCP%d", //NOI18N + getClass().getName(), + System.currentTimeMillis()/1000)); + workDir.mkdirs(); + try { + final File modulesDir = new File(workDir,"modules"); //NOI18N + modulesDir.mkdirs(); + + final Project project = new Project(); + project.init(); + project.setBaseDir(workDir); + final MockCommandLauncher mockProcLauncher = new MockCommandLauncher(); + project.addReference( + MagicNames.ANT_VM_LAUNCHER_REF_ID, + mockProcLauncher); + JUnitTask task = new JUnitTask(); + task.setDir(workDir); + task.setFork(true); + task.setProject(project); + final File junit = LoaderUtils.getResourceSource( + JUnitTask.class.getClassLoader(), + "junit/framework/Test.class"); //NOI18N + final Path cp = new Path(project); + cp.setPath(junit.getAbsolutePath()); + task.createClasspath().add(cp); + final Path mp = new Path(project); + mp.setPath(modulesDir.getName()); + task.createModulepath().add(mp); + task.addTest(new JUnitTest("org.apache.tools.ant.taskdefs.optional.junit.TestTest")); + task.execute(); + assertNotNull(mockProcLauncher.cmd); + String resCp = null; + String resMp = null; + Set resExports = new TreeSet<>(); + for (int i = 1; i< mockProcLauncher.cmd.length; i++) { + if ("-classpath".equals(mockProcLauncher.cmd[i])) { //NOI18N + resCp = mockProcLauncher.cmd[++i]; + } else if ("-modulepath".equals(mockProcLauncher.cmd[i])) { //NOI18N + resMp = mockProcLauncher.cmd[++i]; + } else if (mockProcLauncher.cmd[i].startsWith("-XaddExports:")) { //NOI18N + resExports.add(mockProcLauncher.cmd[i]); + } else if (JUnitTestRunner.class.getName().equals(mockProcLauncher.cmd[i])) { + break; + } + } + assertTrue("No exports", resExports.isEmpty()); + assertEquals("Expected classpath", cp.toString(), resCp); + assertEquals("Expected modulepath", mp.toString(), resMp); + } finally { + delete(workDir); + } + } + + @Test + public void testJunitOnMpArguments() throws Exception { + final File tmp = new File(System.getProperty("java.io.tmpdir")); //NOI18N + final File workDir = new File(tmp, String.format("%s_testJMP%d", //NOI18N + getClass().getName(), + System.currentTimeMillis()/1000)); + workDir.mkdirs(); + try { + final File modulesDir = new File(workDir,"modules"); //NOI18N + modulesDir.mkdirs(); + + final Project project = new Project(); + project.init(); + project.setBaseDir(workDir); + final MockCommandLauncher mockProcLauncher = new MockCommandLauncher(); + project.addReference( + MagicNames.ANT_VM_LAUNCHER_REF_ID, + mockProcLauncher); + JUnitTask task = new JUnitTask(); + task.setDir(workDir); + task.setFork(true); + task.setProject(project); + final File junit = LoaderUtils.getResourceSource( + JUnitTask.class.getClassLoader(), + "junit/framework/Test.class"); //NOI18N + final Path mp = new Path(project); + mp.add(new Path(project, junit.getAbsolutePath())); + mp.add(new Path(project, modulesDir.getName())); + task.createModulepath().add(mp); + task.addTest(new JUnitTest("org.apache.tools.ant.taskdefs.optional.junit.TestTest")); //NOI18N + task.execute(); + assertNotNull(mockProcLauncher.cmd); + String resCp = null; + String resMp = null; + Set resExports = new TreeSet<>(); + for (int i = 1; i< mockProcLauncher.cmd.length; i++) { + if ("-classpath".equals(mockProcLauncher.cmd[i])) { //NOI18N + resCp = mockProcLauncher.cmd[++i]; + } else if ("-modulepath".equals(mockProcLauncher.cmd[i])) { //NOI18N + resMp = mockProcLauncher.cmd[++i]; + } else if (mockProcLauncher.cmd[i].startsWith("-XaddExports:")) { //NOI18N + resExports.add(mockProcLauncher.cmd[i]); + } else if (JUnitTestRunner.class.getName().equals(mockProcLauncher.cmd[i])) { + break; + } + } + assertTrue("No exports", resExports.isEmpty()); + assertNull("No classpath", resCp); + assertEquals("Expected modulepath", mp.toString(), resMp); + } finally { + delete(workDir); + } + } + + private void delete(File f) { + if (f.isDirectory()) { + final File[] clds = f.listFiles(); + if (clds != null) { + for (File cld : clds) { + delete(cld); + } + } + } + f.delete(); + } + + private static final class MockCommandLauncher extends CommandLauncher { + private String[] cmd; + + @Override + public Process exec(Project project, String[] cmd, String[] env, File workingDir) throws IOException { + this.cmd = Arrays.copyOf(cmd, cmd.length); + return new MockProcess(); + } + + private static class MockProcess extends Process { + + @Override + public OutputStream getOutputStream() { + return new ByteArrayOutputStream(); + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(new byte[0]); + } + + @Override + public InputStream getErrorStream() { + return new ByteArrayInputStream(new byte[0]); + } + + @Override + public int waitFor() throws InterruptedException { + return exitValue(); + } + + @Override + public int exitValue() { + return 0; + } + + @Override + public void destroy() { + } + } + } }