changeset 16498:e15c8b97a107

8071566: Improve testing for multi-version JAR file maker tool Reviewed-by: chegar
author anazarov
date Wed, 18 Jan 2017 20:39:08 +0300
parents 028f221e28a7
children 304eb67a5ab3
files test/tools/jar/multiRelease/ApiValidatorTest.java test/tools/jar/multiRelease/Basic.java test/tools/jar/multiRelease/Basic1.java test/tools/jar/multiRelease/MRTestBase.java test/tools/jar/multiRelease/data/test04/v9/version/Version.java
diffstat 5 files changed, 553 insertions(+), 458 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/tools/jar/multiRelease/ApiValidatorTest.java	Wed Jan 18 20:39:08 2017 +0300
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @summary Tests for API validator.
+ * @library /test/lib /lib/testlibrary
+ * @modules java.base/jdk.internal.misc
+ *          jdk.compiler
+ *          jdk.jartool
+ * @build jdk.test.lib.JDKToolFinder jdk.test.lib.Utils jdk.test.lib.process.*
+ * @build jdk.testlibrary.FileUtils
+ * @build MRTestBase
+ * @run testng ApiValidatorTest
+ */
+
+import jdk.test.lib.process.OutputAnalyzer;
+import jdk.testlibrary.FileUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class ApiValidatorTest extends MRTestBase {
+
+    @Test(dataProvider = "signatureChange")
+    public void changeMethodSignature(String sigBase, String sigV10,
+                                      boolean isAcceptable,
+                                      Method method) throws Throwable {
+        Path root = Paths.get(method.getName());
+        Path classes = root.resolve("classes");
+
+        String METHOD_SIG = "#SIG";
+        String classTemplate =
+                "public class C { \n" +
+                        "    " + METHOD_SIG + "{ throw new RuntimeException(); };\n" +
+                        "}\n";
+        String base = classTemplate.replace(METHOD_SIG, sigBase);
+        String v10 = classTemplate.replace(METHOD_SIG, sigV10);
+
+        compileTemplate(classes.resolve("base"), base);
+        compileTemplate(classes.resolve("v10"), v10);
+
+        String jarfile = root.resolve("test.jar").toString();
+        OutputAnalyzer result = jar("cf", jarfile,
+                "-C", classes.resolve("base").toString(), ".",
+                "--release", "10", "-C", classes.resolve("v10").toString(),
+                ".");
+        if (isAcceptable) {
+            result.shouldHaveExitValue(SUCCESS)
+                    .shouldBeEmpty();
+        } else {
+            result.shouldNotHaveExitValue(SUCCESS)
+                    .shouldContain("contains a class with different api from earlier version");
+        }
+
+        FileUtils.deleteFileTreeWithRetry(root);
+    }
+
+    @DataProvider
+    Object[][] signatureChange() {
+        return new Object[][]{
+                {"public int m()", "protected int m()", false},
+                {"protected int m()", "public int m()", false},
+                {"public int m()", "int m()", false},
+                {"protected int m()", "private int m()", false},
+                {"private int m()", "int m()", true},
+                {"int m()", "private int m()", true},
+                {"int m()", "private int m(boolean b)", true},
+                {"public int m()", "public int m(int i)", false},
+                {"public int m()", "public int k()", false},
+                {"public int m()", "private int k()", false},
+// @ignore JDK-8172147   {"public int m()", "public boolean m()", false},
+// @ignore JDK-8172147   {"public boolean", "public Boolean", false},
+// @ignore JDK-8172147   {"public <T> T", "public <T extends String> T", false},
+        };
+    }
+
+    @Test(dataProvider = "publicAPI")
+    public void introducingPublicMembers(String publicAPI,
+                                         Method method) throws Throwable {
+        Path root = Paths.get(method.getName());
+        Path classes = root.resolve("classes");
+
+        String API = "#API";
+        String classTemplate =
+                "public class C { \n" +
+                        "    " + API + "\n" +
+                        "    public void method(){ };\n" +
+                        "}\n";
+        String base = classTemplate.replace(API, "");
+        String v10 = classTemplate.replace(API, publicAPI);
+
+        compileTemplate(classes.resolve("base"), base);
+        compileTemplate(classes.resolve("v10"), v10);
+
+        String jarfile = root.resolve("test.jar").toString();
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                "--release", "10", "-C", classes.resolve("v10").toString(), ".")
+                .shouldNotHaveExitValue(SUCCESS)
+                .shouldContain("contains a class with different api from earlier version");
+
+        FileUtils.deleteFileTreeWithRetry(root);
+    }
+
+    @DataProvider
+    Object[][] publicAPI() {
+        return new Object[][]{
+// @ignore JDK-8172148  {"protected class Inner { public void m(){ } } "}, // protected inner class
+// @ignore JDK-8172148  {"public class Inner { public void m(){ } }"},  // public inner class
+// @ignore JDK-8172148  {"public enum E { A; }"},  // public enum
+                {"public void m(){ }"}, // public method
+                {"protected void m(){ }"}, // protected method
+        };
+    }
+
+    @Test(dataProvider = "privateAPI")
+    public void introducingPrivateMembers(String privateAPI,
+                                          Method method) throws Throwable {
+        Path root = Paths.get(method.getName());
+        Path classes = root.resolve("classes");
+
+        String API = "#API";
+        String classTemplate =
+                "public class C { \n" +
+                        "    " + API + "\n" +
+                        "    public void method(){ };\n" +
+                        "}\n";
+        String base = classTemplate.replace(API, "");
+        String v10 = classTemplate.replace(API, privateAPI);
+
+        compileTemplate(classes.resolve("base"), base);
+        compileTemplate(classes.resolve("v10"), v10);
+
+        String jarfile = root.resolve("test.jar").toString();
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                "--release", "10", "-C", classes.resolve("v10").toString(), ".")
+                .shouldHaveExitValue(SUCCESS);
+        // add release
+        jar("uf", jarfile,
+                "--release", "11", "-C", classes.resolve("v10").toString(), ".")
+                .shouldHaveExitValue(SUCCESS);
+        // replace release
+        jar("uf", jarfile,
+                "--release", "11", "-C", classes.resolve("v10").toString(), ".")
+                .shouldHaveExitValue(SUCCESS);
+
+        FileUtils.deleteFileTreeWithRetry(root);
+    }
+
+    @DataProvider
+    Object[][] privateAPI() {
+        return new Object[][]{
+                {"private class Inner { public void m(){ } } "}, // private inner class
+                {"class Inner { public void m(){ } }"},  // package private inner class
+                {"enum E { A; }"},  // package private enum
+                // Local class and private method
+                {"private void m(){ class Inner { public void m(){} } Inner i = null; }"},
+                {"void m(){ }"}, // package private method
+        };
+    }
+
+    private void compileTemplate(Path classes, String template) throws Throwable {
+        Path classSourceFile = Files.createDirectories(
+                classes.getParent().resolve("src").resolve(classes.getFileName()))
+                .resolve("C.java");
+        Files.write(classSourceFile, template.getBytes());
+        javac(classes, classSourceFile);
+    }
+}
\ No newline at end of file
--- a/test/tools/jar/multiRelease/Basic.java	Wed Jan 18 08:03:04 2017 -0800
+++ b/test/tools/jar/multiRelease/Basic.java	Wed Jan 18 20:39:08 2017 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,69 +23,59 @@
 
 /*
  * @test
- * @library /test/lib
+ * @library /test/lib /lib/testlibrary
  * @modules java.base/jdk.internal.misc
  *          jdk.compiler
  *          jdk.jartool
- * @build jdk.test.lib.JDKToolFinder jdk.test.lib.Utils
+ * @build jdk.test.lib.JDKToolFinder jdk.test.lib.Utils jdk.test.lib.process.*
+ * @build jdk.testlibrary.FileUtils
+ * @build MRTestBase
  * @run testng Basic
  */
 
 import static org.testng.Assert.*;
 
+import jdk.testlibrary.FileUtils;
 import org.testng.annotations.*;
 
-import java.io.*;
+import java.io.File;
 import java.nio.file.*;
-import java.nio.file.attribute.*;
 import java.util.*;
-import java.util.function.Consumer;
-import java.util.jar.*;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.zip.*;
+import java.util.jar.JarFile;
+import java.util.zip.ZipFile;
 
-import jdk.test.lib.JDKToolFinder;
-import jdk.test.lib.Utils;
-
-
-import static java.lang.String.format;
-import static java.lang.System.out;
-
-public class Basic {
-    private final String src = System.getProperty("test.src", ".");
-    private final String usr = System.getProperty("user.dir", ".");
+public class Basic extends MRTestBase {
 
     @Test
     // create a regular, non-multi-release jar
-    public void test00() throws IOException {
+    public void test00() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
 
         Path classes = Paths.get("classes");
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, false);
 
-        Map<String,String[]> names = Map.of(
+        Map<String, String[]> names = Map.of(
                 "version/Main.class",
-                new String[] {"base", "version", "Main.class"},
+                new String[]{"base", "version", "Main.class"},
 
                 "version/Version.class",
-                new String[] {"base", "version", "Version.class"}
+                new String[]{"base", "version", "Version.class"}
         );
 
         compare(jarfile, names);
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // create a multi-release jar
-    public void test01() throws IOException {
+    public void test01() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");
@@ -94,68 +84,96 @@
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".",
                 "--release", "10", "-C", classes.resolve("v10").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, true);
 
-        Map<String,String[]> names = Map.of(
+        Map<String, String[]> names = Map.of(
                 "version/Main.class",
-                new String[] {"base", "version", "Main.class"},
+                new String[]{"base", "version", "Main.class"},
 
                 "version/Version.class",
-                new String[] {"base", "version", "Version.class"},
+                new String[]{"base", "version", "Version.class"},
 
                 "META-INF/versions/9/version/Version.class",
-                new String[] {"v9", "version", "Version.class"},
+                new String[]{"v9", "version", "Version.class"},
 
                 "META-INF/versions/10/version/Version.class",
-                new String[] {"v10", "version", "Version.class"}
+                new String[]{"v10", "version", "Version.class"}
         );
 
         compare(jarfile, names);
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
+    }
+
+    @Test
+    public void versionFormat() throws Throwable {
+        String jarfile = "test.jar";
+
+        compile("test01");
+
+        Path classes = Paths.get("classes");
+
+        // valid
+        for (String release : List.of("10000", "09", "00010", "10")) {
+            jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                    "--release", release, "-C", classes.resolve("v10").toString(), ".")
+                    .shouldHaveExitValue(SUCCESS)
+                    .shouldBeEmpty();
+        }
+        // invalid
+        for (String release : List.of("9.0", "8", "v9",
+                "9v", "0", "-10")) {
+            jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                    "--release", release, "-C", classes.resolve("v10").toString(), ".")
+                    .shouldNotHaveExitValue(SUCCESS)
+                    .shouldContain("release " + release + " not valid");
+        }
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // update a regular jar to a multi-release jar
-    public void test02() throws IOException {
+    public void test02() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
 
         Path classes = Paths.get("classes");
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, false);
 
-        jar("uf", jarfile, "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess();
+        jar("uf", jarfile,
+                "--release", "9", "-C", classes.resolve("v9").toString(), ".")
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, true);
 
-        Map<String,String[]> names = Map.of(
+        Map<String, String[]> names = Map.of(
                 "version/Main.class",
-                new String[] {"base", "version", "Main.class"},
+                new String[]{"base", "version", "Main.class"},
 
                 "version/Version.class",
-                new String[] {"base", "version", "Version.class"},
+                new String[]{"base", "version", "Version.class"},
 
                 "META-INF/versions/9/version/Version.class",
-                new String[] {"v9", "version", "Version.class"}
+                new String[]{"v9", "version", "Version.class"}
         );
 
         compare(jarfile, names);
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // replace a base entry and a versioned entry
-    public void test03() throws IOException {
+    public void test03() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -163,19 +181,19 @@
         Path classes = Paths.get("classes");
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, true);
 
-        Map<String,String[]> names = Map.of(
+        Map<String, String[]> names = Map.of(
                 "version/Main.class",
-                new String[] {"base", "version", "Main.class"},
+                new String[]{"base", "version", "Main.class"},
 
                 "version/Version.class",
-                new String[] {"base", "version", "Version.class"},
+                new String[]{"base", "version", "Version.class"},
 
                 "META-INF/versions/9/version/Version.class",
-                new String[] {"v9", "version", "Version.class"}
+                new String[]{"v9", "version", "Version.class"}
         );
 
         compare(jarfile, names);
@@ -184,25 +202,25 @@
         // version/Version.class entry in versions/9 section
         jar("uf", jarfile, "-C", classes.resolve("v9").toString(), "version",
                 "--release", "9", "-C", classes.resolve("v10").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, true);
 
         names = Map.of(
                 "version/Main.class",
-                new String[] {"base", "version", "Main.class"},
+                new String[]{"base", "version", "Main.class"},
 
                 "version/Version.class",
-                new String[] {"v9", "version", "Version.class"},
+                new String[]{"v9", "version", "Version.class"},
 
                 "META-INF/versions/9/version/Version.class",
-                new String[] {"v10", "version", "Version.class"}
+                new String[]{"v10", "version", "Version.class"}
         );
 
         compare(jarfile, names);
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     /*
@@ -211,7 +229,7 @@
 
     @Test
     // META-INF/versions/9 class has different api than base class
-    public void test04() throws IOException {
+    public void test04() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -224,18 +242,16 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertFailure()
-                .resultChecker(r ->
-                    assertTrue(r.output.contains("different api from earlier"), r.output)
-                );
+                .shouldNotHaveExitValue(SUCCESS)
+                .shouldContain("different api from earlier");
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // META-INF/versions/9 contains an extra public class
-    public void test05() throws IOException {
+    public void test05() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -248,18 +264,16 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertFailure()
-                .resultChecker(r ->
-                        assertTrue(r.output.contains("contains a new public class"), r.output)
-                );
+                .shouldNotHaveExitValue(SUCCESS)
+                .shouldContain("contains a new public class");
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // META-INF/versions/9 contains an extra package private class -- this is okay
-    public void test06() throws IOException {
+    public void test06() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -272,16 +286,16 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // META-INF/versions/9 contains an identical class to base entry class
     // this is okay but produces warning
-    public void test07() throws IOException {
+    public void test07() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -294,19 +308,42 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess()
-                .resultChecker(r ->
-                        assertTrue(r.outputContains("contains a class that is identical"), r.output)
-                );
+                .shouldHaveExitValue(SUCCESS)
+                .shouldContain("contains a class that")
+                .shouldContain("is identical");
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
+    }
+
+    @Test
+    // META-INF/versions/9 contains an identical class to previous version entry class
+    // this is okay but produces warning
+    public void identicalClassToPreviousVersion() throws Throwable {
+        String jarfile = "test.jar";
+
+        compile("test01");  //use same data as test01
+
+        Path classes = Paths.get("classes");
+
+        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+                "--release", "9", "-C", classes.resolve("v9").toString(), ".")
+                .shouldHaveExitValue(SUCCESS)
+                .shouldBeEmpty();
+        jar("uf", jarfile,
+                "--release", "10", "-C", classes.resolve("v9").toString(), ".")
+                .shouldHaveExitValue(SUCCESS)
+                .shouldContain("contains a class that")
+                .shouldContain("is identical");
+
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // resources with same name in different versions
     // this is okay but produces warning
-    public void test08() throws IOException {
+    public void test08() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -320,10 +357,8 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess()
-                .resultChecker(r ->
-                        assertTrue(r.output.isEmpty(), r.output)
-                );
+                .shouldHaveExitValue(SUCCESS)
+                .shouldBeEmpty();
 
         // now add a different resource with same name to META-INF/version/9
         Files.copy(source.resolve("Main.java"), classes.resolve("v9")
@@ -331,18 +366,16 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess()
-                .resultChecker(r ->
-                        assertTrue(r.output.contains("multiple resources with same name"), r.output)
-                );
+                .shouldHaveExitValue(SUCCESS)
+                .shouldContain("multiple resources with same name");
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // a class with an internal name different from the external name
-    public void test09() throws IOException {
+    public void test09() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -355,18 +388,16 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertFailure()
-                .resultChecker(r ->
-                        assertTrue(r.output.contains("names do not match"), r.output)
-                );
+                .shouldNotHaveExitValue(SUCCESS)
+                .shouldContain("names do not match");
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // assure that basic nested classes are acceptable
-    public void test10() throws IOException {
+    public void test10() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -383,15 +414,15 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertSuccess();
+                .shouldHaveExitValue(SUCCESS);
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // a base entry contains a nested class that doesn't have a matching top level class
-    public void test11() throws IOException {
+    public void test11() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -409,30 +440,29 @@
         source = Paths.get(src, "data", "test10", "v9", "version");
         javac(classes.resolve("v9"), source.resolve("Nested.java"));
 
-        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
+        List<String> output = jar("cf", jarfile,
+                "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertFailure()
-                .resultChecker(r -> {
-                    String[] msg = r.output.split("\\R");
-                    // There should be 3 error messages, cascading from the first.  Once we
-                    // remove the base top level class, the base nested class becomes isolated,
-                    // also the versioned top level class becomes a new public class, thus ignored
-                    // for subsequent checks, leading to the associated versioned nested class
-                    // becoming an isolated nested class
-                    assertTrue(msg.length == 4);
-                    assertTrue(msg[0].contains("an isolated nested class"), msg[0]);
-                    assertTrue(msg[1].contains("contains a new public class"), msg[1]);
-                    assertTrue(msg[2].contains("an isolated nested class"), msg[2]);
-                    assertTrue(msg[3].contains("invalid multi-release jar file"), msg[3]);
-                });
+                .shouldNotHaveExitValue(SUCCESS)
+                .asLines();
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        assertTrue(output.size() == 4);
+        assertTrue(output.get(0).contains("an isolated nested class"),
+                output.get(0));
+        assertTrue(output.get(1).contains("contains a new public class"),
+                output.get(1));
+        assertTrue(output.get(2).contains("an isolated nested class"),
+                output.get(2));
+        assertTrue(output.get(3).contains("invalid multi-release jar file"),
+                output.get(3));
+
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
     @Test
     // a versioned entry contains a nested class that doesn't have a matching top level class
-    public void test12() throws IOException {
+    public void test12() throws Throwable {
         String jarfile = "test.jar";
 
         compile("test01");  //use same data as test01
@@ -452,178 +482,59 @@
 
         jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
                 "--release", "9", "-C", classes.resolve("v9").toString(), ".")
-                .assertFailure()
-                .resultChecker(r ->
-                        assertTrue(r.outputContains("an isolated nested class"), r.output)
-                );
+                .shouldNotHaveExitValue(SUCCESS)
+                .shouldContain("an isolated nested class");
 
-        delete(jarfile);
-        deleteDir(Paths.get(usr, "classes"));
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 
-    /*
-     *  Test Infrastructure
-     */
-    private void compile(String test) throws IOException {
-        Path classes = Paths.get(usr, "classes", "base");
-        Files.createDirectories(classes);
-        Path source = Paths.get(src, "data", test, "base", "version");
-        javac(classes, source.resolve("Main.java"), source.resolve("Version.java"));
+    @Test
+    public void testCustomManifest() throws Throwable {
+        String jarfile = "test.jar";
 
-        classes = Paths.get(usr, "classes", "v9");
-        Files.createDirectories(classes);
-        source = Paths.get(src, "data", test, "v9", "version");
-        javac(classes, source.resolve("Version.java"));
+        compile("test01");
 
-        classes = Paths.get(usr, "classes", "v10");
-        Files.createDirectories(classes);
-        source = Paths.get(src, "data", test, "v10", "version");
-        javac(classes, source.resolve("Version.java"));
-    }
+        Path classes = Paths.get("classes");
+        Path manifest = Paths.get("Manifest.txt");
 
-    private void checkMultiRelease(String jarFile, boolean expected) throws IOException {
-        try (JarFile jf = new JarFile(new File(jarFile), true, ZipFile.OPEN_READ,
-                JarFile.runtimeVersion())) {
-            assertEquals(jf.isMultiRelease(), expected);
-        }
-    }
+        // create
+        Files.write(manifest, "Class-Path: MyUtils.jar\n".getBytes());
 
-    // compares the bytes found in the jar entries with the bytes found in the
-    // corresponding data files used to create the entries
-    private void compare(String jarfile, Map<String,String[]> names) throws IOException {
-        try (JarFile jf = new JarFile(jarfile)) {
-            for (String name : names.keySet()) {
-                Path path = Paths.get("classes", names.get(name));
-                byte[] b1 = Files.readAllBytes(path);
-                byte[] b2;
-                JarEntry je = jf.getJarEntry(name);
-                try (InputStream is = jf.getInputStream(je)) {
-                    b2 = is.readAllBytes();
-                }
-                assertEquals(b1,b2);
-            }
-        }
-    }
+        jar("cfm", jarfile, manifest.toString(),
+                "-C", classes.resolve("base").toString(), ".",
+                "--release", "10", "-C", classes.resolve("v10").toString(), ".")
+                .shouldHaveExitValue(SUCCESS)
+                .shouldBeEmpty();
 
-    private void delete(String name) throws IOException {
-        Files.deleteIfExists(Paths.get(usr, name));
-    }
-
-    private void deleteDir(Path dir) throws IOException {
-        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
-            @Override
-            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                Files.delete(file);
-                return FileVisitResult.CONTINUE;
-            }
-
-            @Override
-            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
-                Files.delete(dir);
-                return FileVisitResult.CONTINUE;
-            }
-        });
-    }
-
-    /*
-     * The following methods were taken from modular jar and other jar tests
-     */
-
-    void javac(Path dest, Path... sourceFiles) throws IOException {
-        String javac = JDKToolFinder.getJDKTool("javac");
-
-        List<String> commands = new ArrayList<>();
-        commands.add(javac);
-        String opts = System.getProperty("test.compiler.opts");
-        if (!opts.isEmpty()) {
-            commands.addAll(Arrays.asList(opts.split(" +")));
-        }
-        commands.add("-d");
-        commands.add(dest.toString());
-        Stream.of(sourceFiles).map(Object::toString).forEach(x -> commands.add(x));
-
-        quickFail(run(new ProcessBuilder(commands)));
-    }
-
-    Result jarWithStdin(File stdinSource, String... args) {
-        String jar = JDKToolFinder.getJDKTool("jar");
-        List<String> commands = new ArrayList<>();
-        commands.add(jar);
-        commands.addAll(Utils.getForwardVmOptions());
-        Stream.of(args).forEach(x -> commands.add(x));
-        ProcessBuilder p = new ProcessBuilder(commands);
-        if (stdinSource != null)
-            p.redirectInput(stdinSource);
-        return run(p);
-    }
-
-    Result jar(String... args) {
-        return jarWithStdin(null, args);
-    }
-
-    void quickFail(Result r) {
-        if (r.ec != 0)
-            throw new RuntimeException(r.output);
-    }
-
-    Result run(ProcessBuilder pb) {
-        Process p;
-        out.printf("Running: %s%n", pb.command());
-        try {
-            p = pb.start();
-        } catch (IOException e) {
-            throw new RuntimeException(
-                    format("Couldn't start process '%s'", pb.command()), e);
+        try (JarFile jf = new JarFile(new File(jarfile), true,
+                ZipFile.OPEN_READ, JarFile.runtimeVersion())) {
+            assertTrue(jf.isMultiRelease(), "Not multi-release jar");
+            assertEquals(jf.getManifest()
+                            .getMainAttributes()
+                            .getValue("Class-Path"),
+                    "MyUtils.jar");
         }
 
-        String output;
-        try {
-            output = toString(p.getInputStream(), p.getErrorStream());
-        } catch (IOException e) {
-            throw new RuntimeException(
-                    format("Couldn't read process output '%s'", pb.command()), e);
+        // update
+        Files.write(manifest, "Multi-release: false\n".getBytes());
+
+        jar("ufm", jarfile, manifest.toString(),
+                "-C", classes.resolve("base").toString(), ".",
+                "--release", "9", "-C", classes.resolve("v10").toString(), ".")
+                .shouldHaveExitValue(SUCCESS)
+                .shouldContain("WARNING: Duplicate name in Manifest: Multi-release.");
+
+        try (JarFile jf = new JarFile(new File(jarfile), true,
+                ZipFile.OPEN_READ, JarFile.runtimeVersion())) {
+            assertTrue(jf.isMultiRelease(), "Not multi-release jar");
+            assertEquals(jf.getManifest()
+                            .getMainAttributes()
+                            .getValue("Class-Path"),
+                    "MyUtils.jar");
         }
 
-        try {
-            p.waitFor();
-        } catch (InterruptedException e) {
-            throw new RuntimeException(
-                    format("Process hasn't finished '%s'", pb.command()), e);
-        }
-        return new Result(p.exitValue(), output);
-    }
-
-    String toString(InputStream in1, InputStream in2) throws IOException {
-        try (ByteArrayOutputStream dst = new ByteArrayOutputStream();
-             InputStream concatenated = new SequenceInputStream(in1, in2)) {
-            concatenated.transferTo(dst);
-            return new String(dst.toByteArray(), "UTF-8");
-        }
-    }
-
-    static class Result {
-        final int ec;
-        final String output;
-
-        private Result(int ec, String output) {
-            this.ec = ec;
-            this.output = output;
-        }
-
-        boolean outputContains(String msg) {
-            return Arrays.stream(output.split("\\R"))
-                         .collect(Collectors.joining(" "))
-                         .contains(msg);
-        }
-
-        Result assertSuccess() {
-            assertTrue(ec == 0, format("ec: %d, output: %s", ec, output));
-            return this;
-        }
-        Result assertFailure() {
-            assertTrue(ec != 0, format("ec: %d, output: %s", ec, output));
-            return this;
-        }
-        Result resultChecker(Consumer<Result> r) { r.accept(this); return this; }
+        FileUtils.deleteFileIfExistsWithRetry(Paths.get(jarfile));
+        FileUtils.deleteFileTreeWithRetry(Paths.get(usr, "classes"));
     }
 }
--- a/test/tools/jar/multiRelease/Basic1.java	Wed Jan 18 08:03:04 2017 -0800
+++ b/test/tools/jar/multiRelease/Basic1.java	Wed Jan 18 20:39:08 2017 +0300
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -28,76 +28,65 @@
  *          jdk.compiler
  *          jdk.jartool
  * @build jdk.test.lib.JDKToolFinder jdk.test.lib.Utils
+ * @build MRTestBase
  * @run testng Basic1
  */
 
-import static org.testng.Assert.*;
-
 import org.testng.annotations.*;
 
-import java.io.*;
 import java.nio.file.*;
 import java.util.*;
-import java.util.function.Consumer;
-import java.util.jar.*;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.zip.*;
 
-import jdk.test.lib.JDKToolFinder;
-import jdk.test.lib.Utils;
-
-
-import static java.lang.String.format;
-import static java.lang.System.out;
-
-public class Basic1 {
-    private final String src = System.getProperty("test.src", ".");
+public class Basic1 extends MRTestBase {
 
     @BeforeTest
-    public void setup() throws IOException {
+    public void setup() throws Throwable {
         String test = "test01";
-        Path classes = Paths.get("classes", "base");
-        Files.createDirectories(classes);
+        Path classes = Paths.get("classes");
+
+        Path base = classes.resolve("base");
+        Files.createDirectories(base);
         Path source = Paths.get(src, "data", test, "base", "version");
-        javac(classes, source.resolve("Main.java"), source.resolve("Version.java"));
+        javac(base, source.resolve("Main.java"), source.resolve("Version.java"));
 
-        Path v9 = Paths.get("v9");
+        Path v9 = classes.resolve("v9");
         Files.createDirectories(v9);
         source = Paths.get(src, "data", test, "v9", "version");
         javac(v9, source.resolve("Version.java"));
 
-        Path v10 = Paths.get("v10");
+        Path v10 = classes.resolve("v10");
         Files.createDirectories(v10);
         source = Paths.get(src, "data", test, "v10", "version");
         javac(v10, source.resolve("Version.java"));
 
-        Path v10_1 = Paths.get("v10_1").resolve("META-INF").resolve("versions").resolve("v10");
+        Path v10_1 = classes.resolve("v10_1").resolve("META-INF").resolve("versions").resolve("v10");
         Files.createDirectories(v10_1);
         source = Paths.get(src, "data", test, "v10", "version");
         javac(v10_1, source.resolve("Version.java"));
     }
 
     @Test
-    public void test() throws IOException {
+    public void test() throws Throwable {
         String jarfile = "test.jar";
         Path classes = Paths.get("classes");
-        Path v9 = Paths.get("v9");
-        Path v10 = Paths.get("v10");
 
-        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
-            "--release", "9", "-C", v9.toString(), ".",
-            "--release", "10", "-C", v10.toString(), ".")
-            .assertSuccess();
+        Path base = classes.resolve("base");
+        Path v9 = classes.resolve("v9");
+        Path v10 = classes.resolve("v10");
+
+        jar("cf", jarfile, "-C", base.toString(), ".",
+                "--release", "9", "-C", v9.toString(), ".",
+                "--release", "10", "-C", v10.toString(), ".")
+                .shouldHaveExitValue(SUCCESS);
 
         checkMultiRelease(jarfile, true);
 
-        Map<String,String[]> names = Map.of(
-            "version/Main.class",
-            new String[] {"classes", "base", "version", "Main.class"},
+        Map<String, String[]> names = Map.of(
+                "version/Main.class",
+                new String[]{"base", "version", "Main.class"},
 
-            "version/Version.class",
-            new String[] {"classes", "base", "version", "Version.class"},
+                "version/Version.class",
+                new String[]{"base", "version", "Version.class"},
 
             "META-INF/versions/9/version/Version.class",
             new String[] {"v9", "version", "Version.class"},
@@ -109,144 +98,16 @@
         compare(jarfile, names);
     }
 
-
     @Test
-    public void testFail() throws IOException {
+    public void testFail() throws Throwable {
         String jarfile = "test.jar";
         Path classes = Paths.get("classes");
-        Path v10 = Paths.get("v10_1");
+        Path base = classes.resolve("base");
+        Path v10 = classes.resolve("v10_1");
 
-        jar("cf", jarfile, "-C", classes.resolve("base").toString(), ".",
-            "--release", "10", "-C", v10.toString(), ".")
-            .assertFailure()
-            .outputContains("unexpected versioned entry META-INF/versions/");
-    }
-
-
-
-    private void checkMultiRelease(String jarFile, boolean expected) throws IOException {
-        try (JarFile jf = new JarFile(new File(jarFile), true, ZipFile.OPEN_READ,
-                JarFile.runtimeVersion())) {
-            assertEquals(jf.isMultiRelease(), expected);
-        }
-    }
-
-    // compares the bytes found in the jar entries with the bytes found in the
-    // corresponding data files used to create the entries
-    private void compare(String jarfile, Map<String,String[]> names) throws IOException {
-        try (JarFile jf = new JarFile(jarfile)) {
-            for (String name : names.keySet()) {
-                Path path = Paths.get("", names.get(name));
-                byte[] b1 = Files.readAllBytes(path);
-                byte[] b2;
-                JarEntry je = jf.getJarEntry(name);
-                try (InputStream is = jf.getInputStream(je)) {
-                    b2 = is.readAllBytes();
-                }
-                assertEquals(b1,b2);
-            }
-        }
-    }
-
-    /*
-     * The following methods were taken from modular jar and other jar tests
-     */
-
-    void javac(Path dest, Path... sourceFiles) throws IOException {
-        String javac = JDKToolFinder.getJDKTool("javac");
-
-        List<String> commands = new ArrayList<>();
-        commands.add(javac);
-        String opts = System.getProperty("test.compiler.opts");
-        if (!opts.isEmpty()) {
-            commands.addAll(Arrays.asList(opts.split(" +")));
-        }
-        commands.add("-d");
-        commands.add(dest.toString());
-        Stream.of(sourceFiles).map(Object::toString).forEach(x -> commands.add(x));
-
-        quickFail(run(new ProcessBuilder(commands)));
-    }
-
-    Result jarWithStdin(File stdinSource, String... args) {
-        String jar = JDKToolFinder.getJDKTool("jar");
-        List<String> commands = new ArrayList<>();
-        commands.add(jar);
-        commands.addAll(Utils.getForwardVmOptions());
-        Stream.of(args).forEach(x -> commands.add(x));
-        ProcessBuilder p = new ProcessBuilder(commands);
-        if (stdinSource != null)
-            p.redirectInput(stdinSource);
-        return run(p);
-    }
-
-    Result jar(String... args) {
-        return jarWithStdin(null, args);
-    }
-
-    void quickFail(Result r) {
-        if (r.ec != 0)
-            throw new RuntimeException(r.output);
-    }
-
-    Result run(ProcessBuilder pb) {
-        Process p;
-        out.printf("Running: %s%n", pb.command());
-        try {
-            p = pb.start();
-        } catch (IOException e) {
-            throw new RuntimeException(
-                    format("Couldn't start process '%s'", pb.command()), e);
-        }
-
-        String output;
-        try {
-            output = toString(p.getInputStream(), p.getErrorStream());
-        } catch (IOException e) {
-            throw new RuntimeException(
-                    format("Couldn't read process output '%s'", pb.command()), e);
-        }
-
-        try {
-            p.waitFor();
-        } catch (InterruptedException e) {
-            throw new RuntimeException(
-                    format("Process hasn't finished '%s'", pb.command()), e);
-        }
-        return new Result(p.exitValue(), output);
-    }
-
-    String toString(InputStream in1, InputStream in2) throws IOException {
-        try (ByteArrayOutputStream dst = new ByteArrayOutputStream();
-             InputStream concatenated = new SequenceInputStream(in1, in2)) {
-            concatenated.transferTo(dst);
-            return new String(dst.toByteArray(), "UTF-8");
-        }
-    }
-
-    static class Result {
-        final int ec;
-        final String output;
-
-        private Result(int ec, String output) {
-            this.ec = ec;
-            this.output = output;
-        }
-
-        boolean outputContains(String msg) {
-            return Arrays.stream(output.split("\\R"))
-                         .collect(Collectors.joining(" "))
-                         .contains(msg);
-        }
-
-        Result assertSuccess() {
-            assertTrue(ec == 0, format("ec: %d, output: %s", ec, output));
-            return this;
-        }
-        Result assertFailure() {
-            assertTrue(ec != 0, format("ec: %d, output: %s", ec, output));
-            return this;
-        }
-        Result resultChecker(Consumer<Result> r) { r.accept(this); return this; }
+        jar("cf", jarfile, "-C", base.toString(), ".",
+                "--release", "10", "-C", v10.toString(), ".")
+                .shouldNotHaveExitValue(SUCCESS)
+                .shouldContain("unexpected versioned entry META-INF/versions/");
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/tools/jar/multiRelease/MRTestBase.java	Wed Jan 18 20:39:08 2017 +0300
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import jdk.test.lib.JDKToolFinder;
+import jdk.test.lib.Utils;
+import jdk.test.lib.process.OutputAnalyzer;
+import jdk.test.lib.process.ProcessTools;
+
+import java.io.*;
+import java.nio.file.*;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
+import java.util.zip.ZipFile;
+
+import static org.testng.Assert.assertEquals;
+
+public class MRTestBase {
+
+    public static final int SUCCESS = 0;
+
+    protected final String src = System.getProperty("test.src", ".");
+    protected final String usr = System.getProperty("user.dir", ".");
+
+    protected void compile(String test) throws Throwable {
+        Path classes = Paths.get(usr, "classes", "base");
+        Files.createDirectories(classes);
+        Path source = Paths.get(src, "data", test, "base", "version");
+        javac(classes, source.resolve("Main.java"), source.resolve("Version.java"));
+
+        classes = Paths.get(usr, "classes", "v9");
+        Files.createDirectories(classes);
+        source = Paths.get(src, "data", test, "v9", "version");
+        javac(classes, source.resolve("Version.java"));
+
+        classes = Paths.get(usr, "classes", "v10");
+        Files.createDirectories(classes);
+        source = Paths.get(src, "data", test, "v10", "version");
+        javac(classes, source.resolve("Version.java"));
+    }
+
+    protected void checkMultiRelease(String jarFile,
+                                     boolean expected) throws IOException {
+        try (JarFile jf = new JarFile(new File(jarFile), true,
+                ZipFile.OPEN_READ, JarFile.runtimeVersion())) {
+            assertEquals(jf.isMultiRelease(), expected);
+        }
+    }
+
+    // compares the bytes found in the jar entries with the bytes found in the
+    // corresponding data files used to create the entries
+    protected void compare(String jarfile,
+                           Map<String, String[]> names) throws IOException {
+        try (JarFile jf = new JarFile(jarfile)) {
+            for (String name : names.keySet()) {
+                Path path = Paths.get("classes", names.get(name));
+                byte[] b1 = Files.readAllBytes(path);
+                byte[] b2;
+                JarEntry je = jf.getJarEntry(name);
+                try (InputStream is = jf.getInputStream(je)) {
+                    b2 = is.readAllBytes();
+                }
+                assertEquals(b1, b2);
+            }
+        }
+    }
+
+    void javac(Path dest, Path... sourceFiles) throws Throwable {
+        String javac = JDKToolFinder.getJDKTool("javac");
+
+        List<String> commands = new ArrayList<>();
+        commands.add(javac);
+        String opts = System.getProperty("test.compiler.opts");
+        if (!opts.isEmpty()) {
+            commands.addAll(Arrays.asList(opts.split(" +")));
+        }
+        commands.addAll(Utils.getForwardVmOptions());
+        commands.add("-d");
+        commands.add(dest.toString());
+        Stream.of(sourceFiles)
+                .map(Object::toString)
+                .forEach(x -> commands.add(x));
+
+        ProcessTools.executeCommand(new ProcessBuilder(commands))
+                .shouldHaveExitValue(SUCCESS);
+    }
+
+    OutputAnalyzer jarWithStdin(File stdinSource,
+                                String... args) throws Throwable {
+
+        String jar = JDKToolFinder.getJDKTool("jar");
+        List<String> commands = new ArrayList<>();
+        commands.add(jar);
+        commands.addAll(Utils.getForwardVmOptions());
+        Stream.of(args).forEach(x -> commands.add(x));
+        ProcessBuilder p = new ProcessBuilder(commands);
+        if (stdinSource != null)
+            p.redirectInput(stdinSource);
+        return ProcessTools.executeCommand(p);
+    }
+
+    OutputAnalyzer jar(String... args) throws Throwable {
+        return jarWithStdin(null, args);
+    }
+}
\ No newline at end of file
--- a/test/tools/jar/multiRelease/data/test04/v9/version/Version.java	Wed Jan 18 08:03:04 2017 -0800
+++ b/test/tools/jar/multiRelease/data/test04/v9/version/Version.java	Wed Jan 18 20:39:08 2017 +0300
@@ -8,7 +8,7 @@
     protected void doNothing() {
     }
 
-    // extra publc method
+    // extra public method
     public void anyName() {
     }
 }