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>(); | ||||
@@ -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 an enumeration listing each test, then each batchtest | ||||
* @return enumeration | * @return enumeration | ||||
@@ -1886,16 +1980,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()); | ||||
@@ -2274,4 +2375,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() { | |||||
} | |||||
} | |||||
} | |||||
} | } |