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> | <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> | <h4>formatter</h4> | ||||
| <p>The results of the tests can be printed in different | <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 | The two nested formatters are for displaying (for the user) and for updating the collector | ||||
| class. | class. | ||||
| </p> | </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> | </body> | ||||
| </html> | </html> | ||||
| @@ -38,6 +38,7 @@ import java.util.HashMap; | |||||
| import java.util.Hashtable; | import java.util.Hashtable; | ||||
| import java.util.Iterator; | import java.util.Iterator; | ||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.Locale; | |||||
| import java.util.Map; | import java.util.Map; | ||||
| import java.util.Properties; | import java.util.Properties; | ||||
| import java.util.Vector; | import java.util.Vector; | ||||
| @@ -510,6 +511,26 @@ public class JUnitTask extends Task { | |||||
| return getCommandline().createBootclasspath(getProject()).createPath(); | 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. | * Adds an environment variable; used when forking. | ||||
| * | * | ||||
| @@ -749,7 +770,7 @@ public class JUnitTask extends Task { | |||||
| loader.loadClass("junit.framework.Test"); // sanity check | loader.loadClass("junit.framework.Test"); // sanity check | ||||
| } catch (final ClassNotFoundException e) { | } catch (final ClassNotFoundException e) { | ||||
| throw new BuildException( | 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", | + "if not in Ant's own classpath", | ||||
| e, task.getLocation()); | e, task.getLocation()); | ||||
| } | } | ||||
| @@ -777,10 +798,14 @@ public class JUnitTask extends Task { | |||||
| if (splitJUnit) { | if (splitJUnit) { | ||||
| final Path path = new Path(getProject()); | final Path path = new Path(getProject()); | ||||
| path.add(antRuntimeClasses); | path.add(antRuntimeClasses); | ||||
| final Path extra = getCommandline().getClasspath(); | |||||
| Path extra = getCommandline().getClasspath(); | |||||
| if (extra != null) { | if (extra != null) { | ||||
| path.add(extra); | path.add(extra); | ||||
| } | } | ||||
| extra = getCommandline().getModulepath(); | |||||
| if (extra != null && !hasJunit(path)) { | |||||
| path.add(expandModulePath(extra)); | |||||
| } | |||||
| mirrorLoader = (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() { | mirrorLoader = (ClassLoader) AccessController.doPrivileged(new PrivilegedAction() { | ||||
| public Object run() { | public Object run() { | ||||
| return new SplitClassLoader(myLoader, path, getProject(), | return new SplitClassLoader(myLoader, path, getProject(), | ||||
| @@ -818,7 +843,7 @@ public class JUnitTask extends Task { | |||||
| @Override | @Override | ||||
| public void execute() throws BuildException { | public void execute() throws BuildException { | ||||
| checkMethodLists(); | checkMethodLists(); | ||||
| checkModules(); | |||||
| setupJUnitDelegate(); | setupJUnitDelegate(); | ||||
| final List<List> testLists = new ArrayList<List>(); | final List<List> testLists = new ArrayList<List>(); | ||||
| @@ -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 an enumeration listing each test, then each batchtest | ||||
| * @return enumeration | * @return enumeration | ||||
| @@ -1892,16 +1986,23 @@ public class JUnitTask extends Task { | |||||
| */ | */ | ||||
| private void createClassLoader() { | private void createClassLoader() { | ||||
| final Path userClasspath = getCommandline().getClasspath(); | final Path userClasspath = getCommandline().getClasspath(); | ||||
| if (userClasspath != null) { | |||||
| final Path userModulepath = getCommandline().getModulepath(); | |||||
| if (userClasspath != null || userModulepath != null) { | |||||
| if (reloading || classLoader == null) { | if (reloading || classLoader == null) { | ||||
| deleteClassLoader(); | 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) { | if (includeAntRuntime) { | ||||
| log("Implicitly adding " + antRuntimeClasses | log("Implicitly adding " + antRuntimeClasses | ||||
| + " to CLASSPATH", Project.MSG_VERBOSE); | + " to CLASSPATH", Project.MSG_VERBOSE); | ||||
| classpath.append(antRuntimeClasses); | |||||
| path.append(antRuntimeClasses); | |||||
| } | } | ||||
| classLoader = getProject().createClassLoader(classpath); | |||||
| classLoader = getProject().createClassLoader(path); | |||||
| if (getClass().getClassLoader() != null | if (getClass().getClassLoader() != null | ||||
| && getClass().getClassLoader() != Project.class.getClassLoader()) { | && getClass().getClassLoader() != Project.class.getClassLoader()) { | ||||
| classLoader.setParent(getClass().getClassLoader()); | classLoader.setParent(getClass().getClassLoader()); | ||||
| @@ -2280,4 +2381,24 @@ public class JUnitTask extends Task { | |||||
| w.newLine(); | w.newLine(); | ||||
| s.println(text); | 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 static org.apache.tools.ant.AntAssert.assertContains; | ||||
| import java.io.BufferedReader; | import java.io.BufferedReader; | ||||
| import java.io.ByteArrayInputStream; | |||||
| import java.io.ByteArrayOutputStream; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.FileReader; | import java.io.FileReader; | ||||
| import java.io.IOException; | 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.DocumentBuilder; | ||||
| import javax.xml.parsers.DocumentBuilderFactory; | import javax.xml.parsers.DocumentBuilderFactory; | ||||
| import javax.xml.xpath.XPath; | import javax.xml.xpath.XPath; | ||||
| import javax.xml.xpath.XPathConstants; | import javax.xml.xpath.XPathConstants; | ||||
| import javax.xml.xpath.XPathFactory; | import javax.xml.xpath.XPathFactory; | ||||
| import org.apache.tools.ant.BuildException; | |||||
| import org.apache.tools.ant.BuildFileRule; | 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.JavaEnvUtils; | ||||
| import org.apache.tools.ant.util.LoaderUtils; | |||||
| import org.junit.Assume; | import org.junit.Assume; | ||||
| import org.junit.Before; | import org.junit.Before; | ||||
| import org.junit.Rule; | 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() { | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||