this is the combined patch or #18 which couldn't be applied via `git am`master
| @@ -371,6 +371,21 @@ subelement.</p> | |||
| <p><em>since Ant 1.6.</em></p> | |||
| <h4>modulepath</h4> | |||
| <p>The location of modules can be specified using this <a href="../using.html#path">PATH like structure</a>.<br/> | |||
| The modulepath requires <i>fork</i> to be set to <code>true</code>. | |||
| <p><em>since Ant 1.10</em></p> | |||
| <h4>upgrademodulepath</h4> | |||
| <p>The location of modules that replace upgradeable modules in the runtime image | |||
| can be specified using this <a href="../using.html#path">PATH like structure</a>.<br/> | |||
| The upgrademodulepath requires <i>fork</i> to be set to <code>true</code>. | |||
| <p><em>since Ant 1.10</em></p> | |||
| <h4>formatter</h4> | |||
| <p>The results of the tests can be printed in different | |||
| @@ -796,7 +811,47 @@ the single <code><test/></code> 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. | |||
| </p> | |||
| <pre> | |||
| <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> | |||
| </pre> | |||
| <p>Runs my.test.TestCase as a white-box test in the forked VM given by the <code>platform.java</code> 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 <code>module.name</code> property.<br/> | |||
| The <code>-Xpatch</code> java option executes the tests built into <code>${build.test.classes}</code> in a module given | |||
| by <code>module.name</code> property.<br/> | |||
| The <code>-addmods</code> java option enables the tested module.<br/> | |||
| The <code>-XaddReads</code> java option makes the unnamed module containing the junit readable by tested module.<br/> | |||
| The <code>-XaddExports</code> java option makes the non-exported test package <code>my.test</code> accessible from the unnamed module containing the junit.<br/> | |||
| <pre> | |||
| <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> | |||
| </pre> | |||
| <p>Runs my.test.TestCase as a black-box test in the forked VM given by the <code>platform.java</code> property. | |||
| The junit library is used as an automatic module. The tests module-info requires the tested module and junit.<br/> | |||
| The <code>-addmods</code> java option enables the test module.<br/> | |||
| The <code>-XaddExports</code> java option makes the non-exported test package <code>my.test</code> accessible from the junit module and Ant's test runner. | |||
| Another possibility is to export the test package in the tests module-info by <code>exports my.test</code> directive.<br/> | |||
| </body> | |||
| </html> | |||
| @@ -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 <classpath> for <junit> must include junit.jar " | |||
| "The <classpath> or <modulepath> for <junit> 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<List> testLists = new ArrayList<List>(); | |||
| @@ -1691,6 +1716,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 | |||
| @@ -1886,16 +1980,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()); | |||
| @@ -2274,4 +2375,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 <code>null</code> 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 | |||
| } | |||
| } | |||
| @@ -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<String> 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<String> 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() { | |||
| } | |||
| } | |||
| } | |||
| } | |||