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() { | |||
} | |||
} | |||
} | |||
} |