/* * 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 * 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 * @bug 8146486 8172432 * @summary Fail to create a MR modular JAR with a versioned entry in * base-versioned empty package * @modules java.base/jdk.internal.module * jdk.compiler * jdk.jartool * @library /test/lib * @build jdk.test.lib.util.FileUtils * jdk.test.lib.Utils * jdk.test.lib.Asserts * jdk.test.lib.JDKToolFinder * jdk.test.lib.JDKToolLauncher * jdk.test.lib.Platform * jdk.test.lib.process.* * @run testng Basic */ import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.Test; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.UncheckedIOException; import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleDescriptor.Version; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Optional; import java.util.Set; import java.util.spi.ToolProvider; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipFile; import jdk.internal.module.ModuleInfoExtender; import jdk.test.lib.util.FileUtils; public class Basic { private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar") .orElseThrow(() -> new RuntimeException("jar tool not found")); private static final ToolProvider JAVAC_TOOL = ToolProvider.findFirst("javac") .orElseThrow(() -> new RuntimeException("javac tool not found")); private final String linesep = System.lineSeparator(); private final Path testsrc; private final Path userdir; private final ByteArrayOutputStream outbytes = new ByteArrayOutputStream(); private final PrintStream out = new PrintStream(outbytes, true); private final ByteArrayOutputStream errbytes = new ByteArrayOutputStream(); private final PrintStream err = new PrintStream(errbytes, true); public Basic() throws IOException { testsrc = Paths.get(System.getProperty("test.src")); userdir = Paths.get(System.getProperty("user.dir", ".")); // compile the classes directory Path source = testsrc.resolve("src").resolve("classes"); Path destination = Paths.get("classes"); javac(source, destination); // compile the mr9 directory including module-info.java source = testsrc.resolve("src").resolve("mr9"); destination = Paths.get("mr9"); javac(source, destination); // move module-info.class for later use Files.move(destination.resolve("module-info.class"), Paths.get("module-info.class")); } private void javac(Path source, Path destination) throws IOException { String[] args = Stream.concat( Stream.of("-d", destination.toString()), Files.walk(source) .map(Path::toString) .filter(s -> s.endsWith(".java")) ).toArray(String[]::new); JAVAC_TOOL.run(System.out, System.err, args); } private int jar(String cmd) { outbytes.reset(); errbytes.reset(); return JAR_TOOL.run(out, err, cmd.split(" +")); } @AfterClass public void cleanup() throws IOException { Files.walk(userdir, 1) .filter(p -> !p.equals(userdir)) .forEach(p -> { try { if (Files.isDirectory(p)) { FileUtils.deleteFileTreeWithRetry(p); } else { FileUtils.deleteFileIfExistsWithRetry(p); } } catch (IOException x) { throw new UncheckedIOException(x); } }); } // updates a valid multi-release jar with a new public class in // versioned section and fails @Test public void test1() { // successful build of multi-release jar int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 p/Hi.class"); Assert.assertEquals(rc, 0); jar("-tf mmr.jar"); Set actual = lines(outbytes); Set expected = Set.of( "META-INF/", "META-INF/MANIFEST.MF", "p/", "p/Hi.class", "META-INF/versions/9/p/Hi.class" ); Assert.assertEquals(actual, expected); // failed build because of new public class rc = jar("-uf mmr.jar --release 9 -C mr9 p/internal/Bar.class"); Assert.assertEquals(rc, 1); String s = new String(errbytes.toByteArray()); Assert.assertTrue(Message.NOT_FOUND_IN_BASE_ENTRY.match(s, "p/internal/Bar.class")); } // updates a valid multi-release jar with a module-info class and new // concealed public class in versioned section and succeeds @Test public void test2() { // successful build of multi-release jar int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 p/Hi.class"); Assert.assertEquals(rc, 0); // successful build because of module-info and new public class rc = jar("-uf mmr.jar module-info.class --release 9 -C mr9 p/internal/Bar.class"); Assert.assertEquals(rc, 0); String s = new String(errbytes.toByteArray()); Assert.assertTrue(Message.NEW_CONCEALED_PACKAGE_WARNING.match(s, "p/internal/Bar.class")); jar("-tf mmr.jar"); Set actual = lines(outbytes); Set expected = Set.of( "META-INF/", "META-INF/MANIFEST.MF", "p/", "p/Hi.class", "META-INF/versions/9/p/Hi.class", "META-INF/versions/9/p/internal/Bar.class", "module-info.class" ); Assert.assertEquals(actual, expected); } // jar tool fails building mmr.jar because of new public class @Test public void test3() { int rc = jar("-cf mmr.jar -C classes . --release 9 -C mr9 ."); Assert.assertEquals(rc, 1); String s = new String(errbytes.toByteArray()); Assert.assertTrue(Message.NOT_FOUND_IN_BASE_ENTRY.match(s, "p/internal/Bar.class")); } // jar tool succeeds building mmr.jar because of concealed package @Test public void test4() { int rc = jar("-cf mmr.jar module-info.class -C classes . " + "--release 9 module-info.class -C mr9 ."); Assert.assertEquals(rc, 0); String s = new String(errbytes.toByteArray()); Assert.assertTrue(Message.NEW_CONCEALED_PACKAGE_WARNING.match(s, "p/internal/Bar.class")); jar("-tf mmr.jar"); Set actual = lines(outbytes); Set expected = Set.of( "META-INF/", "META-INF/MANIFEST.MF", "module-info.class", "META-INF/versions/9/module-info.class", "p/", "p/Hi.class", "META-INF/versions/9/", "META-INF/versions/9/p/", "META-INF/versions/9/p/Hi.class", "META-INF/versions/9/p/internal/", "META-INF/versions/9/p/internal/Bar.class" ); Assert.assertEquals(actual, expected); } // jar tool does two updates, no exported packages, all concealed. // Along with various --describe-module variants @Test public void test5() throws IOException { // compile the mr10 directory Path source = testsrc.resolve("src").resolve("mr10"); Path destination = Paths.get("mr10"); javac(source, destination); // create a directory for this tests special files Files.createDirectory(Paths.get("test5")); // create an empty module-info.java String hi = "module hi {" + linesep + "}" + linesep; Path modinfo = Paths.get("test5", "module-info.java"); Files.write(modinfo, hi.getBytes()); // and compile it javac(modinfo, Paths.get("test5")); int rc = jar("--create --file mr.jar -C classes ."); Assert.assertEquals(rc, 0); rc = jar("--update --file mr.jar -C test5 module-info.class" + " --release 9 -C mr9 ."); Assert.assertEquals(rc, 0); jar("tf mr.jar"); Set actual = lines(outbytes); Set expected = Set.of( "META-INF/", "META-INF/MANIFEST.MF", "p/", "p/Hi.class", "META-INF/versions/9/", "META-INF/versions/9/p/", "META-INF/versions/9/p/Hi.class", "META-INF/versions/9/p/internal/", "META-INF/versions/9/p/internal/Bar.class", "module-info.class" ); Assert.assertEquals(actual, expected); jar("-d --file mr.jar"); String uri = (Paths.get("mr.jar")).toUri().toString(); uri = "jar:" + uri + "/!module-info.class"; actual = lines(outbytes); expected = Set.of( "hi " + uri, "requires java.base mandated", "contains p", "contains p.internal" ); Assert.assertEquals(actual, expected); rc = jar("--update --file mr.jar --release 10 -C mr10 ."); Assert.assertEquals(rc, 0); jar("tf mr.jar"); actual = lines(outbytes); expected = Set.of( "META-INF/", "META-INF/MANIFEST.MF", "p/", "p/Hi.class", "META-INF/versions/9/", "META-INF/versions/9/p/", "META-INF/versions/9/p/Hi.class", "META-INF/versions/9/p/internal/", "META-INF/versions/9/p/internal/Bar.class", "META-INF/versions/10/", "META-INF/versions/10/p/", "META-INF/versions/10/p/internal/", "META-INF/versions/10/p/internal/bar/", "META-INF/versions/10/p/internal/bar/Gee.class", "module-info.class" ); Assert.assertEquals(actual, expected); jar("-d --file mr.jar"); actual = lines(outbytes); expected = Set.of( "hi " + uri, "requires java.base mandated", "contains p", "contains p.internal", "contains p.internal.bar" ); Assert.assertEquals(actual, expected); for (String release : new String[] {"9" , "10", "100", "1000"}) { jar("-d --file mr.jar --release " + release); actual = lines(outbytes); Assert.assertEquals(actual, expected); } } // root and versioned module-info entries have different main-class, version // attributes @Test public void test6() throws IOException { // create a directory for this tests special files Files.createDirectory(Paths.get("test6")); Files.createDirectory(Paths.get("test6-v9")); // compile the classes directory Path src = testsrc.resolve("src").resolve("classes"); Path dst = Paths.get("test6"); javac(src, dst); byte[] mdBytes = Files.readAllBytes(Paths.get("module-info.class")); ModuleInfoExtender mie = ModuleInfoExtender.newExtender( new ByteArrayInputStream(mdBytes)); mie.mainClass("p.Main"); mie.version(Version.parse("1.0")); ByteArrayOutputStream baos = new ByteArrayOutputStream(); mie.write(baos); Files.write(Paths.get("test6", "module-info.class"), baos.toByteArray()); Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray()); int rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 ."); Assert.assertEquals(rc, 0); // different main-class mie = ModuleInfoExtender.newExtender(new ByteArrayInputStream(mdBytes)); mie.mainClass("p.Main2"); mie.version(Version.parse("1.0")); baos.reset(); mie.write(baos); Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray()); rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 ."); Assert.assertEquals(rc, 1); Assert.assertTrue(Message.CONTAINS_DIFFERENT_MAINCLASS.match( new String(errbytes.toByteArray()), "META-INF/versions/9/module-info.class")); // different version mie = ModuleInfoExtender.newExtender(new ByteArrayInputStream(mdBytes)); mie.mainClass("p.Main"); mie.version(Version.parse("2.0")); baos.reset(); mie.write(baos); Files.write(Paths.get("test6-v9", "module-info.class"), baos.toByteArray()); rc = jar("--create --file mmr.jar -C test6 . --release 9 -C test6-v9 ."); Assert.assertEquals(rc, 1); Assert.assertTrue(Message.CONTAINS_DIFFERENT_VERSION.match( new String(errbytes.toByteArray()), "META-INF/versions/9/module-info.class")); } // versioned mmr without root module-info.class @Test public void test7() throws IOException { // create a directory for this tests special files Files.createDirectory(Paths.get("test7")); Files.createDirectory(Paths.get("test7-v9")); Files.createDirectory(Paths.get("test7-v10")); // compile the classes directory Path src = testsrc.resolve("src").resolve("classes"); Path dst = Paths.get("test7"); javac(src, dst); // move module-info.class to v9 later use Files.copy(Paths.get("module-info.class"), Paths.get("test7-v9", "module-info.class")); Files.copy(Paths.get("test7-v9", "module-info.class"), Paths.get("test7-v10", "module-info.class")); int rc = jar("--create --file mmr.jar --main-class=p.Main -C test7 . --release 9 -C test7-v9 . --release 10 -C test7-v10 ."); Assert.assertEquals(rc, 0); jar("-d --file=mmr.jar"); Set actual = lines(outbytes); Set expected = Set.of( "releases: 9 10", "No root module descriptor, specify --release" ); Assert.assertEquals(actual, expected); String uriPrefix = "jar:" + (Paths.get("mmr.jar")).toUri().toString(); jar("-d --file=mmr.jar --release 9"); actual = lines(outbytes); expected = Set.of( "releases: 9 10", "m1 " + uriPrefix + "/!META-INF/versions/9/module-info.class", "requires java.base mandated", "exports p", "main-class p.Main" ); Assert.assertEquals(actual, expected); jar("-d --file=mmr.jar --release 10"); actual = lines(outbytes); expected = Set.of( "releases: 9 10", "m1 " + uriPrefix + "/!META-INF/versions/10/module-info.class", "requires java.base mandated", "exports p", "main-class p.Main" ); Assert.assertEquals(actual, expected); for (String release : new String[] {"11", "12", "15", "100"}) { jar("-d --file mmr.jar --release " + release); actual = lines(outbytes); Assert.assertEquals(actual, expected); } Optional exp = Optional.of("p.Main"); try (ZipFile zf = new ZipFile("mmr.jar")) { Assert.assertTrue(zf.getEntry("module-info.class") == null); ModuleDescriptor md = ModuleDescriptor.read( zf.getInputStream(zf.getEntry("META-INF/versions/9/module-info.class"))); Assert.assertEquals(md.mainClass(), exp); md = ModuleDescriptor.read( zf.getInputStream(zf.getEntry("META-INF/versions/10/module-info.class"))); Assert.assertEquals(md.mainClass(), exp); } } private static Set lines(ByteArrayOutputStream baos) { String s = new String(baos.toByteArray()); return Arrays.stream(s.split("\\R")) .map(l -> l.trim()) .filter(l -> l.length() > 0) .collect(Collectors.toSet()); } static enum Message { CONTAINS_DIFFERENT_MAINCLASS( ": module-info.class in a versioned directory contains different \"main-class\"" ), CONTAINS_DIFFERENT_VERSION( ": module-info.class in a versioned directory contains different \"version\"" ), NOT_FOUND_IN_BASE_ENTRY( ", contains a new public class not found in base entries" ), NEW_CONCEALED_PACKAGE_WARNING( " is a public class" + " in a concealed package, placing this jar on the class path will result" + " in incompatible public interfaces" ); final String msg; Message(String msg) { this.msg = msg; } /* * Test if the given output contains this message ignoring the line break. */ boolean match(String output, String entry) { System.out.println("Expected: " + entry + msg); System.out.println("Found: " + output); return Arrays.stream(output.split("\\R")) .collect(Collectors.joining(" ")) .contains(entry + msg); } } }